erro ao realizar a tarefa

This commit is contained in:
MeuNome
2026-04-28 17:14:36 +01:00
parent 0fe31e3f65
commit 84e4a7f8b0
18 changed files with 591 additions and 5302 deletions

View File

@@ -30,6 +30,7 @@
<activity android:name=".StatisticsActivity" />
<activity android:name=".FindFriendsActivity" />
<activity android:name=".TrophiesActivity" />
<activity android:name=".MyFriendsActivity" />

View File

@@ -4,9 +4,14 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;

View File

@@ -24,6 +24,18 @@ public class FirestoreManager {
return instance;
}
/**
* Obtém os dados do utilizador uma única vez.
*/
public void getUser(String uid, Consumer<Usuario> callback) {
db.collection("users").document(uid).get().addOnSuccessListener(snapshot -> {
if (snapshot.exists()) {
Usuario user = snapshot.toObject(Usuario.class);
if (user != null) callback.accept(user);
}
});
}
/**
* Observa as mudanças no documento do utilizador em tempo real.
*/
@@ -83,4 +95,16 @@ public class FirestoreManager {
public void updateUserStats(String uid, Map<String, Object> updates) {
db.collection("users").document(uid).update(updates);
}
/**
* Regista um log de XP.
*/
public void addXpLog(String uid, int amount, String type) {
Map<String, Object> log = new java.util.HashMap<>();
log.put("userId", uid);
log.put("amount", amount);
log.put("type", type);
log.put("created_at", com.google.firebase.firestore.FieldValue.serverTimestamp());
db.collection("xp_logs").add(log);
}
}

View File

@@ -12,6 +12,7 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Button;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.ListenerRegistration;
import java.util.HashMap;
@@ -28,6 +29,7 @@ import java.util.Calendar;
import java.util.Locale;
import java.util.ArrayList;
import java.util.List;
import androidx.core.content.ContextCompat;
public class InicioFragment extends Fragment {
@@ -36,7 +38,9 @@ public class InicioFragment extends Fragment {
private TextView tvTodayStreak, tvTodayXP, tvTodayTasksCount, tvNoTasksIncentive;
private TextView tvDailyRewardGoal;
private ProgressBar pbDailyTasksProgress;
private LinearLayout tasksContainer, miniRankingContainer;
private androidx.recyclerview.widget.RecyclerView rvTasks;
private TasksAdapter tasksAdapter;
private LinearLayout miniRankingContainer, layoutTasksSection;
private Button btnStartFocus, btnAddTasks, btnClaimReward;
private CountDownTimer countDownTimer;
@@ -56,7 +60,10 @@ public class InicioFragment extends Fragment {
"Um passo de cada vez.",
"A consistência é a chave.",
"Foca no progresso, não na perfeição.",
"Hoje é um ótimo dia para ser produtivo!"
"Hoje é um ótimo dia para ser produtivo!",
"Vence a procrastinação agora.",
"A tua versão do futuro vai agradecer.",
"Pequenos passos, grandes resultados."
};
@@ -70,7 +77,10 @@ public class InicioFragment extends Fragment {
tvMotivationalSubtitle = view.findViewById(R.id.tvMotivationalSubtitle);
view.findViewById(R.id.cardProfileAvatar).setOnClickListener(v -> {
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).findViewById(R.id.nav_profile).performClick();
BottomNavigationView nav = ((MainActivity) getActivity()).findViewById(R.id.bottom_navigation);
if (nav != null) {
nav.setSelectedItemId(R.id.nav_profile);
}
}
});
@@ -81,7 +91,17 @@ public class InicioFragment extends Fragment {
pbDailyTasksProgress = view.findViewById(R.id.pbDailyTasksProgress);
// Tasks
tasksContainer = view.findViewById(R.id.tasksContainer);
layoutTasksSection = view.findViewById(R.id.layoutTasksSection);
rvTasks = view.findViewById(R.id.rvTasks);
rvTasks.setLayoutManager(new androidx.recyclerview.widget.LinearLayoutManager(getContext()));
tasksAdapter = new TasksAdapter(currentTasks, new TasksAdapter.OnTaskClickListener() {
@Override public void onTaskFocus(Task task) { selectTaskForFocus(task); }
@Override public void onTaskDelete(Task task) { showDeleteConfirmDialog(task); }
@Override public void onTaskEdit(Task task) { showEditTaskDialog(task); }
});
rvTasks.setAdapter(tasksAdapter);
setupSwipeToDelete();
tvNoTasksIncentive = view.findViewById(R.id.tvNoTasksIncentive);
btnAddTasks = view.findViewById(R.id.btnAddTasks);
btnAddTasks.setOnClickListener(v -> showAddTaskDialog());
@@ -92,7 +112,9 @@ public class InicioFragment extends Fragment {
btnStartFocus.setOnClickListener(v -> {
if (!isTimerRunning) {
if (selectedTaskForFocus == null) {
Toast.makeText(getContext(), "Seleciona uma tarefa primeiro!", Toast.LENGTH_SHORT).show();
if (getContext() != null) {
Toast.makeText(getContext(), "Seleciona uma tarefa primeiro!", Toast.LENGTH_SHORT).show();
}
return;
}
startTimer();
@@ -137,7 +159,11 @@ public class InicioFragment extends Fragment {
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
tasksListener = FirestoreManager.getInstance().observeTasks(currentUser.getUid(), tasks -> {
currentTasks = tasks;
List<Task> activeTasks = new ArrayList<>();
for (Task t : tasks) {
if (!t.completed) activeTasks.add(t);
}
currentTasks = activeTasks;
updateTasksUI();
updateTodayCard();
});
@@ -159,131 +185,157 @@ public class InicioFragment extends Fragment {
}
// Update Reward Button State
if (user.tasks_concluidas_hoje >= user.meta_diaria_tarefas) {
String today = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new java.util.Date());
boolean alreadyClaimed = today.equals(user.last_reward_claim_date);
if (user.tasks_concluidas_hoje >= user.meta_diaria_tarefas && !alreadyClaimed) {
btnClaimReward.setEnabled(true);
btnClaimReward.setAlpha(1.0f);
btnClaimReward.setText("Resgatar");
} else if (alreadyClaimed) {
btnClaimReward.setEnabled(false);
btnClaimReward.setAlpha(0.5f);
btnClaimReward.setText("Resgatado");
} else {
btnClaimReward.setEnabled(false);
btnClaimReward.setAlpha(0.5f);
btnClaimReward.setText("Resgatar");
}
checkDailyReset(user);
updateTodayCard(user);
checkDailyResetAndStreak(user);
});
}
}
private void updateTasksUI() {
if (getContext() == null || tasksContainer == null) return;
tasksContainer.removeAllViews();
for (Task task : currentTasks) {
View taskView = LayoutInflater.from(getContext()).inflate(R.layout.item_task_home, tasksContainer, false);
TextView tvTitle = taskView.findViewById(R.id.tvTaskTitle);
TextView tvDuration = taskView.findViewById(R.id.tvTaskDuration);
CheckBox cb = taskView.findViewById(R.id.cbTask);
Button btnStart = taskView.findViewById(R.id.btnStartTaskFocus);
tvTitle.setText(task.title);
tvDuration.setText(task.duration + " min");
cb.setChecked(task.completed);
if (task.completed) {
tvTitle.setPaintFlags(tvTitle.getPaintFlags() | android.graphics.Paint.STRIKE_THRU_TEXT_FLAG);
tvTitle.setTextColor(getResources().getColor(R.color.text_secondary));
btnStart.setEnabled(false);
btnStart.setAlpha(0.5f);
}
cb.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked && !task.completed) {
completeTask(task);
}
});
btnStart.setOnClickListener(v -> {
selectedTaskForFocus = task;
timeLeftInMillis = task.duration * 60 * 1000;
updateCountDownText();
Toast.makeText(getContext(), "Tarefa selecionada: " + task.title, Toast.LENGTH_SHORT).show();
// Highlight selection
for (int i = 0; i < tasksContainer.getChildCount(); i++) {
tasksContainer.getChildAt(i).setBackground(null);
}
taskView.setBackgroundResource(R.drawable.task_selected_bg);
});
taskView.setOnLongClickListener(v -> {
showDeleteConfirmDialog(task);
return true;
});
tasksContainer.addView(taskView);
if (tasksAdapter != null) {
tasksAdapter.setTasks(currentTasks);
}
updateTodayCard();
}
private void selectTaskForFocus(Task task) {
selectedTaskForFocus = task;
timeLeftInMillis = task.duration * 60 * 1000;
updateCountDownText();
if (getContext() != null) {
Toast.makeText(getContext(), "Modo Foco: " + task.title, Toast.LENGTH_SHORT).show();
}
}
private void setupSwipeToDelete() {
new androidx.recyclerview.widget.ItemTouchHelper(new androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback(0, androidx.recyclerview.widget.ItemTouchHelper.LEFT | androidx.recyclerview.widget.ItemTouchHelper.RIGHT) {
@Override
public boolean onMove(@NonNull androidx.recyclerview.widget.RecyclerView recyclerView, @NonNull androidx.recyclerview.widget.RecyclerView.ViewHolder viewHolder, @NonNull androidx.recyclerview.widget.RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull androidx.recyclerview.widget.RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
Task task = currentTasks.get(position);
showDeleteConfirmDialog(task);
tasksAdapter.notifyItemChanged(position); // Revert swipe if canceled? Firestore update will refresh anyway.
}
}).attachToRecyclerView(rvTasks);
}
private void addXP(int amount) {
if (!isAdded()) return;
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
String uid = currentUser.getUid();
Map<String, Object> updates = new HashMap<>();
updates.put("xp", com.google.firebase.firestore.FieldValue.increment(amount));
updates.put("xp_hoje", com.google.firebase.firestore.FieldValue.increment(amount));
updates.put("xp_semanal", com.google.firebase.firestore.FieldValue.increment(amount));
// Check Level Up logic inside observe once
com.google.firebase.firestore.FirebaseFirestore.getInstance()
.collection("users").document(currentUser.getUid()).get()
.addOnSuccessListener(snapshot -> {
Usuario user = snapshot.toObject(Usuario.class);
if (user != null) {
int currentLevel = user.level;
int nextLevelThreshold = currentLevel * 500;
if (user.xp + amount >= nextLevelThreshold) {
updates.put("level", currentLevel + 1);
showLevelUpAnimation(currentLevel + 1);
}
FirestoreManager.getInstance().updateUserStats(currentUser.getUid(), updates);
FirestoreManager.getInstance().getUser(uid, user -> {
if (user != null && isAdded()) {
int currentLevel = user.level;
int threshold = (currentLevel * (currentLevel + 1) / 2) * 100;
if (user.xp + amount >= threshold) {
updates.put("level", currentLevel + 1);
showLevelUpAnimation(currentLevel + 1);
}
});
FirestoreManager.getInstance().updateUserStats(uid, updates);
}
});
}
}
private void updateTodayCard() {
int total = currentTasks.size();
int completed = 0;
for (Task task : currentTasks) {
if (task.completed) completed++;
}
if (tvTodayTasksCount != null) {
tvTodayTasksCount.setText(completed + "/" + (total == 0 ? 3 : total));
}
if (pbDailyTasksProgress != null) {
int progress = (total == 0) ? 0 : (completed * 100) / total;
pbDailyTasksProgress.setProgress(progress);
}
if (total == 0) {
tvNoTasksIncentive.setVisibility(View.VISIBLE);
} else {
tvNoTasksIncentive.setVisibility(View.GONE);
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
FirestoreManager.getInstance().getUser(currentUser.getUid(), this::updateTodayCard);
}
}
private void checkDailyReset(Usuario user) {
private void updateTodayCard(Usuario user) {
int completed = user.tasks_concluidas_hoje;
int goal = user.meta_diaria_tarefas;
if (goal <= 0) goal = 3; // Default fallback
if (tvTodayTasksCount != null) {
tvTodayTasksCount.setText(completed + "/" + goal);
}
if (pbDailyTasksProgress != null) {
int progress = (completed * 100) / goal;
if (progress > 100) progress = 100;
pbDailyTasksProgress.setProgress(progress);
updatePathProgress(progress);
}
if (currentTasks != null && tvNoTasksIncentive != null && layoutTasksSection != null) {
if (currentTasks.isEmpty()) {
tvNoTasksIncentive.setVisibility(View.VISIBLE);
layoutTasksSection.setVisibility(View.GONE);
} else {
tvNoTasksIncentive.setVisibility(View.GONE);
layoutTasksSection.setVisibility(View.VISIBLE);
}
}
}
private void checkDailyResetAndStreak(Usuario user) {
String today = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new java.util.Date());
if (!today.equals(user.last_active_date)) {
Map<String, Object> updates = new HashMap<>();
if (user.tasks_concluidas_hoje == 0 && !user.last_active_date.isEmpty()) {
updates.put("streak", 0);
// Streak logic
if (!user.last_active_date.isEmpty()) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
try {
java.util.Date lastDate = sdf.parse(user.last_active_date);
java.util.Date todayDate = sdf.parse(today);
long diff = (todayDate.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24);
if (diff == 1) {
// Successive day, but wait for first task completion to increment streak?
// Or increment now if they at least logged in? User said "Completar pelo menos 1 tarefa por dia -> streak +1"
// So we don't increment here, we just check if we should reset.
} else if (diff > 1) {
// Failed a day
updates.put("streak", 0);
}
} catch (Exception e) {
e.printStackTrace();
}
}
updates.put("xp_hoje", 0);
updates.put("tasks_concluidas_hoje", 0);
updates.put("tempo_foco_hoje", 0);
updates.put("last_active_date", today);
FirestoreManager.getInstance().updateUserStats(user.id_usuario, updates);
// Login reward
addXP(10);
if (getContext() != null) {
Toast.makeText(getContext(), "Recompensa de login: +10 XP!", Toast.LENGTH_SHORT).show();
}
}
}
@@ -306,14 +358,23 @@ public class InicioFragment extends Fragment {
}
private void addMiniRankingItem(int rank, String name, int xp) {
View item = getLayoutInflater().inflate(android.R.layout.simple_list_item_2, miniRankingContainer, false);
TextView tv1 = item.findViewById(android.R.id.text1);
TextView tv2 = item.findViewById(android.R.id.text2);
tv1.setText(rank + ". " + name);
tv1.setTextSize(14);
tv1.setTextColor(getResources().getColor(R.color.text_primary));
tv2.setText(xp + " XP hoje");
tv2.setTextSize(12);
if (getContext() == null || miniRankingContainer == null) return;
View item = getLayoutInflater().inflate(R.layout.item_ranking_user, miniRankingContainer, false);
TextView tvRank = item.findViewById(R.id.tvRankPosition);
TextView tvName = item.findViewById(R.id.tvRankingName);
TextView tvXP = item.findViewById(R.id.tvRankingXP);
tvRank.setText("#" + rank);
tvName.setText(name);
tvXP.setText(xp + " XP");
// Remove elevation for mini ranking to keep it clean
if (item instanceof androidx.cardview.widget.CardView) {
((androidx.cardview.widget.CardView) item).setCardElevation(0);
((androidx.cardview.widget.CardView) item).setCardBackgroundColor(android.graphics.Color.TRANSPARENT);
}
miniRankingContainer.addView(item);
}
@@ -353,15 +414,19 @@ public class InicioFragment extends Fragment {
if (i < currentDayIndex) {
// Past day - Assume completed for demo
nodeCircle.setBackgroundResource(R.drawable.node_circle_bg);
nodeCircle.getBackground().setTint(getResources().getColor(R.color.success_green));
nodeDayInitial.setTextColor(getResources().getColor(R.color.white));
if (getContext() != null) {
nodeCircle.getBackground().setTint(ContextCompat.getColor(getContext(), R.color.success_green));
nodeDayInitial.setTextColor(ContextCompat.getColor(getContext(), R.color.white));
}
nodeProgress.setVisibility(View.GONE);
} else if (i == currentDayIndex) {
// Today
nodeCircle.setBackgroundResource(R.drawable.node_circle_bg);
nodeCircle.getBackground().setTint(getResources().getColor(R.color.primary_purple));
nodeDayInitial.setTextColor(getResources().getColor(R.color.white));
nodeDayLabel.setTextColor(getResources().getColor(R.color.primary_purple));
if (getContext() != null) {
nodeCircle.getBackground().setTint(ContextCompat.getColor(getContext(), R.color.primary_purple));
nodeDayInitial.setTextColor(ContextCompat.getColor(getContext(), R.color.white));
nodeDayLabel.setTextColor(ContextCompat.getColor(getContext(), R.color.primary_purple));
}
nodeProgress.setVisibility(View.VISIBLE);
} else {
// Future
@@ -377,30 +442,31 @@ public class InicioFragment extends Fragment {
private void completeTask(Task task) {
task.completed = true;
FirestoreManager.getInstance().updateTask(task);
addXP(30);
updateUserTaskCount();
boolean allDone = true;
for (Task t : currentTasks) {
if (!t.completed && !t.id.equals(task.id)) {
allDone = false;
break;
}
}
if (allDone && currentTasks.size() > 0) {
addXP(20);
Toast.makeText(getContext(), "Todas as tarefas concluídas! +20 XP Bónus", Toast.LENGTH_LONG).show();
}
triggerVibration();
showXpPopup("+30 XP");
}
private void updateUserTaskCount() {
if (!isAdded()) return;
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
Map<String, Object> updates = new HashMap<>();
updates.put("tasks_concluidas_hoje", com.google.firebase.firestore.FieldValue.increment(1));
updates.put("total_tasks_concluidas", com.google.firebase.firestore.FieldValue.increment(1));
FirestoreManager.getInstance().updateUserStats(currentUser.getUid(), updates);
String uid = currentUser.getUid();
FirestoreManager.getInstance().getUser(uid, user -> {
if (user != null && isAdded()) {
Map<String, Object> updates = new HashMap<>();
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);
}
});
}
}
@@ -421,10 +487,13 @@ public class InicioFragment extends Fragment {
}
private void showXpPopup(String text) {
Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
if (getContext() != null) {
Toast.makeText(getContext(), text, Toast.LENGTH_SHORT).show();
}
}
private void triggerVibration() {
if (getContext() == null) return;
android.os.Vibrator v = (android.os.Vibrator) getContext().getSystemService(android.content.Context.VIBRATOR_SERVICE);
if (v != null) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
@@ -436,10 +505,22 @@ public class InicioFragment extends Fragment {
}
private void claimDailyReward() {
addXP(100);
btnClaimReward.setEnabled(false);
btnClaimReward.setText("Resgatado");
Toast.makeText(getContext(), "Recompensa diária resgatada! +100 XP", Toast.LENGTH_LONG).show();
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
String today = new java.text.SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new java.util.Date());
Map<String, Object> updates = new HashMap<>();
updates.put("last_reward_claim_date", today);
FirestoreManager.getInstance().updateUserStats(currentUser.getUid(), updates);
addXP(100);
btnClaimReward.setEnabled(false);
btnClaimReward.setText("Resgatado");
btnClaimReward.setAlpha(0.5f);
if (getContext() != null) {
Toast.makeText(getContext(), "Recompensa diária resgatada! +100 XP", Toast.LENGTH_LONG).show();
}
}
}
private void showDeleteConfirmDialog(Task task) {
@@ -459,11 +540,11 @@ public class InicioFragment extends Fragment {
ProgressBar nodeProgress = todayNode.findViewById(R.id.nodeProgress);
View nodeCircle = todayNode.findViewById(R.id.nodeCircle);
if (nodeProgress != null) nodeProgress.setProgress(progress);
if (progress == 100) {
nodeCircle.getBackground().setTint(getResources().getColor(R.color.success_green));
if (progress == 100 && getContext() != null) {
nodeCircle.getBackground().setTint(ContextCompat.getColor(getContext(), R.color.success_green));
triggerSuccessAnimation(todayNode);
} else {
nodeCircle.getBackground().setTint(getResources().getColor(R.color.primary_purple));
} else if (getContext() != null) {
nodeCircle.getBackground().setTint(ContextCompat.getColor(getContext(), R.color.primary_purple));
}
}
@@ -477,7 +558,9 @@ public class InicioFragment extends Fragment {
animatorSet.playTogether(scaleX, scaleY);
animatorSet.start();
Toast.makeText(getContext(), "Dia Completado! 🎉", Toast.LENGTH_SHORT).show();
if (getContext() != null) {
Toast.makeText(getContext(), "Dia Completado! 🎉", Toast.LENGTH_SHORT).show();
}
}
@@ -491,19 +574,123 @@ public class InicioFragment extends Fragment {
}
@Override
public void onFinish() {
android.util.Log.d("FLUXUP_DEBUG", "FOCUS_FINISHED_START");
isTimerRunning = false;
btnStartFocus.setText("Começar Foco");
completeTask(selectedTaskForFocus);
addXP(50);
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser != null) {
Map<String, Object> updates = new HashMap<>();
updates.put("tempo_foco_total", com.google.firebase.firestore.FieldValue.increment(selectedTaskForFocus.duration));
updates.put("tempo_foco_hoje", com.google.firebase.firestore.FieldValue.increment(selectedTaskForFocus.duration));
updates.put("sessoes_foco_completas", com.google.firebase.firestore.FieldValue.increment(1));
FirestoreManager.getInstance().updateUserStats(currentUser.getUid(), updates);
if (getActivity() != null && isAdded()) {
btnStartFocus.setText("Começar Foco");
}
try {
if (selectedTaskForFocus == null) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_FINISHED_ERROR: selectedTask is null");
return;
}
String taskId = selectedTaskForFocus.id;
android.util.Log.d("FLUXUP_DEBUG", "TASK_ID: " + taskId);
FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser == null) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_FINISHED_ERROR: USER_ID is null");
return;
}
String userId = currentUser.getUid();
android.util.Log.d("FLUXUP_DEBUG", "USER_ID: " + userId);
if (taskId == null || taskId.isEmpty()) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_FINISHED_ERROR: task.id is empty");
return;
}
// Início da sincronização segura
android.util.Log.d("FLUXUP_DEBUG", "FOCUS_COMPLETE_START");
FirestoreManager.getInstance().getUser(userId, user -> {
if (user == null) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_COMPLETE_ERROR: User data is null");
return;
}
if (!isAdded()) return;
android.util.Log.d("FLUXUP_DEBUG", "SELECTED_TASK: " + taskId);
android.util.Log.d("FLUXUP_DEBUG", "USER_ID: " + userId);
android.util.Log.d("FLUXUP_DEBUG", "XP_BEFORE: " + user.xp_hoje);
// 1. Log de XP
Map<String, Object> log = new HashMap<>();
log.put("userId", userId);
log.put("amount", 50);
log.put("type", "focus_task");
log.put("created_at", com.google.firebase.firestore.FieldValue.serverTimestamp());
com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("xp_logs")
.add(log)
.addOnSuccessListener(ref -> android.util.Log.d("FLUXUP_DEBUG", "XP_LOG_INSERT_RESULT: SUCCESS"))
.addOnFailureListener(e -> android.util.Log.e("FLUXUP_DEBUG", "XP_LOG_INSERT_RESULT: FAIL - " + e.getMessage()));
// 2. Preparar updates consolidados
Map<String, Object> updates = new HashMap<>();
// Usando os nomes exatos de Usuario.java: xp, xp_hoje, xp_semanal
updates.put("xp", com.google.firebase.firestore.FieldValue.increment(50));
updates.put("xp_hoje", com.google.firebase.firestore.FieldValue.increment(50));
updates.put("xp_semanal", com.google.firebase.firestore.FieldValue.increment(50));
updates.put("tempo_foco_total", com.google.firebase.firestore.FieldValue.increment(selectedTaskForFocus.duration));
updates.put("tempo_foco_hoje", com.google.firebase.firestore.FieldValue.increment(selectedTaskForFocus.duration));
updates.put("sessoes_foco_completas", com.google.firebase.firestore.FieldValue.increment(1));
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);
}
}
// Level Up
int currentLevel = user.level;
int threshold = (currentLevel * (currentLevel + 1) / 2) * 100;
if (user.xp + 50 >= threshold) {
updates.put("level", currentLevel + 1);
}
// 3. Gravar tudo no Backend (User Stats)
com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("users").document(userId)
.update(updates)
.addOnSuccessListener(aVoid -> {
android.util.Log.d("FLUXUP_DEBUG", "USER_XP_UPDATE_RESULT: SUCCESS");
if (isAdded()) {
android.util.Log.d("FLUXUP_DEBUG", "HOME_REFRESH_RESULT: SUCCESS");
}
})
.addOnFailureListener(e -> android.util.Log.e("FLUXUP_DEBUG", "USER_XP_UPDATE_RESULT: FAIL - " + e.getMessage()));
// 4. Concluir tarefa no backend
selectedTaskForFocus.completed = true;
com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("tasks").document(taskId)
.set(selectedTaskForFocus)
.addOnSuccessListener(aVoid -> android.util.Log.d("FLUXUP_DEBUG", "TASK_UPDATE_RESULT: SUCCESS"))
.addOnFailureListener(e -> android.util.Log.e("FLUXUP_DEBUG", "TASK_UPDATE_RESULT: FAIL - " + e.getMessage()));
if (getContext() != null && isAdded()) {
Toast.makeText(getContext(), "Boa! Tarefa concluída +50 XP", Toast.LENGTH_LONG).show();
triggerVibration();
selectedTaskForFocus = null;
timeLeftInMillis = 25 * 60 * 1000;
updateCountDownText();
}
});
} catch (Exception e) {
android.util.Log.e("FLUXUP_DEBUG", "FOCUS_FINISHED_ERROR: " + e.getMessage());
if (getContext() != null && isAdded()) {
Toast.makeText(getContext(), "Erro ao concluir tarefa. Tenta novamente.", Toast.LENGTH_SHORT).show();
}
}
selectedTaskForFocus = null;
}
}.start();
isTimerRunning = true;
@@ -543,6 +730,30 @@ public class InicioFragment extends Fragment {
if (countDownTimer != null) countDownTimer.cancel();
}
private void showEditTaskDialog(Task task) {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_add_task, null);
android.widget.EditText etTitle = dialogView.findViewById(R.id.etTaskTitle);
android.widget.EditText etDuration = dialogView.findViewById(R.id.etTaskDuration);
etTitle.setText(task.title);
etDuration.setText(String.valueOf(task.duration));
new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext())
.setTitle("Editar Tarefa")
.setView(dialogView)
.setNegativeButton("Cancelar", null)
.setPositiveButton("Atualizar", (dialog, which) -> {
String title = etTitle.getText().toString().trim();
String durationStr = etDuration.getText().toString().trim();
if (!title.isEmpty()) {
task.title = title;
if (!durationStr.isEmpty()) task.duration = Integer.parseInt(durationStr);
FirestoreManager.getInstance().updateTask(task);
}
})
.show();
}
private void showAddTaskDialog() {
View dialogView = getLayoutInflater().inflate(R.layout.dialog_add_task, null);
android.widget.EditText etTitle = dialogView.findViewById(R.id.etTaskTitle);
@@ -564,7 +775,14 @@ public class InicioFragment extends Fragment {
return;
}
int duration = 25;
if (!durationStr.isEmpty()) duration = Integer.parseInt(durationStr);
if (!durationStr.isEmpty()) {
try {
duration = Integer.parseInt(durationStr);
} catch (NumberFormatException e) {
etDuration.setError("Inválido");
return;
}
}
saveNewTask(title, duration);
dialog.dismiss();
});

View File

@@ -11,6 +11,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.widget.TextView;
import android.widget.GridLayout;
import android.widget.LinearLayout;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.firestore.ListenerRegistration;
@@ -49,7 +51,7 @@ public class ProfileFragment extends Fragment {
});
view.findViewById(R.id.btnManageFriends).setOnClickListener(v -> {
startActivity(new Intent(getActivity(), FindFriendsActivity.class));
startActivity(new Intent(getActivity(), MyFriendsActivity.class));
});
view.findViewById(R.id.btnViewStatsDetails).setOnClickListener(v -> {
@@ -57,7 +59,7 @@ public class ProfileFragment extends Fragment {
});
view.findViewById(R.id.btnLogout).setOnClickListener(v -> {
AuthManager.getInstance().signOut();
AuthManager.getInstance().logout();
Intent intent = new Intent(getActivity(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
@@ -103,13 +105,17 @@ public class ProfileFragment extends Fragment {
private void renderBadges(Usuario user) {
badgesGrid.removeAllViews();
if (user.streak >= 7) addBadge("🔥", "Semana Ativa");
if (user.total_tasks_concluidas >= 50) addBadge("🏆", "Executor");
if (user.tempo_foco_total >= 600) addBadge("🧘", "Zen");
if (user.level >= 5) addBadge("", "Nível 5");
// 7 days streak
addBadge(user.streak >= 7 ? "🔥" : "🔒", "7 Dias", user.streak >= 7);
// 50 tasks
addBadge(user.total_tasks_concluidas >= 50 ? "🎯" : "🔒", "50 Tarefas", user.total_tasks_concluidas >= 50);
// 10 hours focus (600 min)
addBadge(user.tempo_foco_total >= 600 ? "⏱️" : "🔒", "10 Horas", user.tempo_foco_total >= 600);
// 1000 XP
addBadge(user.xp >= 1000 ? "" : "🔒", "1000 XP", user.xp >= 1000);
}
private void addBadge(String emoji, String name) {
private void addBadge(String emoji, String name, boolean unlocked) {
LinearLayout badge = new LinearLayout(getContext());
badge.setOrientation(LinearLayout.VERTICAL);
badge.setGravity(android.view.Gravity.CENTER);
@@ -117,12 +123,14 @@ public class ProfileFragment extends Fragment {
TextView tvEmoji = new TextView(getContext());
tvEmoji.setText(emoji);
tvEmoji.setTextSize(24);
tvEmoji.setTextSize(32);
if (!unlocked) tvEmoji.setAlpha(0.3f);
TextView tvName = new TextView(getContext());
tvName.setText(name);
tvName.setTextSize(8);
tvName.setTextSize(10);
tvName.setGravity(android.view.Gravity.CENTER);
tvName.setTextColor(getResources().getColor(unlocked ? R.color.text_primary : R.color.text_secondary));
badge.addView(tvEmoji);
badge.addView(tvName);

View File

@@ -12,14 +12,13 @@ import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Arrays;
public class SearchFragment extends Fragment {
private RecyclerView recyclerView;
private ListView recyclerView;
private EditText searchBar;
private ArrayAdapter<String> adapter;
private ArrayList<String> allItems;
@@ -35,7 +34,6 @@ public class SearchFragment extends Fragment {
allItems = new ArrayList<>(Arrays.asList("Item 1", "Item 2", "Item 3", "Another Item", "More Items"));
adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, allItems);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
searchBar.addTextChangedListener(new TextWatcher() {

View File

@@ -12,6 +12,7 @@ import android.widget.ArrayAdapter;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
@@ -36,7 +37,8 @@ public class SettingsActivity extends AppCompatActivity {
private void initSettings() {
// --- CONTA ---
setupClickable(R.id.settingEmail, "Email", FirebaseAuth.getInstance().getCurrentUser().getEmail(), null);
setupClickable(R.id.settingEditProfile, "Editar Perfil", "Nome, Bio, Avatar", v -> showEditProfileDialog());
setupClickable(R.id.settingEmail, "Email", FirebaseAuth.getInstance().getCurrentUser().getEmail(), v -> showChangeEmailDialog());
setupClickable(R.id.settingPassword, "Alterar Palavra-passe", "********", v -> resetPassword());
setupSwitch(R.id.settingPublicProfile, "Perfil Público", "public_profile", true);
@@ -59,7 +61,12 @@ public class SettingsActivity extends AppCompatActivity {
setupClickable(R.id.settingExportData, "Exportar Dados", "JSON/CSV", null);
findViewById(R.id.btnDeleteAccount).setOnClickListener(v -> {
Toast.makeText(this, "Funcionalidade disponível em breve", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(this)
.setTitle("Eliminar Conta")
.setMessage("Tens a certeza? Todos os teus dados serão apagados permanentemente.")
.setNegativeButton("Cancelar", null)
.setPositiveButton("Sim, Eliminar", (dialog, which) -> deleteAccount())
.show();
});
}
@@ -96,6 +103,60 @@ public class SettingsActivity extends AppCompatActivity {
}
}
private void showEditProfileDialog() {
View view = getLayoutInflater().inflate(R.layout.dialog_edit_profile, null);
EditText etName = view.findViewById(R.id.etEditName);
EditText etBio = view.findViewById(R.id.etEditBio);
String uid = FirebaseAuth.getInstance().getUid();
FirestoreManager.getInstance().getUser(uid, user -> {
etName.setText(user.usuario);
etBio.setText(user.bio);
});
new AlertDialog.Builder(this)
.setTitle("Editar Perfil")
.setView(view)
.setPositiveButton("Guardar", (dialog, which) -> {
String name = etName.getText().toString();
String bio = etBio.getText().toString();
java.util.Map<String, Object> updates = new java.util.HashMap<>();
updates.put("usuario", name);
updates.put("bio", bio);
FirestoreManager.getInstance().updateUserStats(uid, updates);
Toast.makeText(this, "Perfil atualizado!", Toast.LENGTH_SHORT).show();
})
.setNegativeButton("Cancelar", null)
.show();
}
private void showChangeEmailDialog() {
EditText input = new EditText(this);
input.setHint("Novo email");
new AlertDialog.Builder(this)
.setTitle("Alterar Email")
.setView(input)
.setPositiveButton("Confirmar", (dialog, which) -> {
String newEmail = input.getText().toString();
FirebaseAuth.getInstance().getCurrentUser().updateEmail(newEmail)
.addOnSuccessListener(aVoid -> Toast.makeText(this, "Email alterado!", Toast.LENGTH_SHORT).show())
.addOnFailureListener(e -> Toast.makeText(this, "Erro: " + e.getMessage(), Toast.LENGTH_LONG).show());
})
.setNegativeButton("Cancelar", null)
.show();
}
private void deleteAccount() {
String uid = FirebaseAuth.getInstance().getUid();
com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("users").document(uid).delete();
FirebaseAuth.getInstance().getCurrentUser().delete()
.addOnSuccessListener(aVoid -> {
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
});
}
private void showDurationDialog(String key, String title, int current) {
EditText input = new EditText(this);
input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER);
@@ -105,9 +166,20 @@ public class SettingsActivity extends AppCompatActivity {
.setTitle(title)
.setView(input)
.setPositiveButton("Guardar", (dialog, which) -> {
int val = Integer.parseInt(input.getText().toString());
sharedPreferences.edit().putInt(key, val).apply();
recreate();
try {
int val = Integer.parseInt(input.getText().toString());
sharedPreferences.edit().putInt(key, val).apply();
// Also update in Firestore if user is logged in
String uid = FirebaseAuth.getInstance().getUid();
if (uid != null) {
java.util.Map<String, Object> updates = new java.util.HashMap<>();
if (key.equals("focus_duration")) updates.put("meta_diaria_foco", val);
FirestoreManager.getInstance().updateUserStats(uid, updates);
}
recreate();
} catch (Exception e) {}
})
.setNegativeButton("Cancelar", null)
.show();

View File

@@ -9,6 +9,9 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import android.widget.Button;
import android.widget.ProgressBar;
import java.util.Calendar;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.ListenerRegistration;
@@ -191,12 +194,3 @@ public class TrophiesActivity extends AppCompatActivity {
if (userListener != null) userListener.remove();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (userListener != null) {
userListener.remove();
}
}
}

View File

@@ -32,6 +32,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_reward_claim_date = ""; // YYYY-MM-DD
public String avatar_url = "";
public Usuario() {}

View File

@@ -98,7 +98,7 @@ public class UsuariosService {
};
for (String title : defaultTasks) {
String taskId = getFirestore().collection("tasks").document().getId();
Task task = new Task(taskId, title, 50, uid);
Task task = new Task(taskId, title, 50, 25, uid);
getFirestore().collection("tasks").document(taskId).set(task);
}
}

View File

@@ -50,6 +50,7 @@
<androidx.cardview.widget.CardView style="@style/SettingsCard">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical">
<include layout="@layout/item_settings_clickable" android:id="@+id/settingEditProfile" />
<include layout="@layout/item_settings_clickable" android:id="@+id/settingEmail" />
<include layout="@layout/item_settings_clickable" android:id="@+id/settingPassword" />
<include layout="@layout/item_settings_switch" android:id="@+id/settingPublicProfile" />

View File

@@ -176,22 +176,42 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 3. 🎯 TAREFAS DO DIA -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="Tarefas do Dia"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<Button
android:id="@+id/btnAddTasks"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginBottom="24dp"
android:background="@drawable/button_primary"
android:text="+ Adicionar Tarefa"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:backgroundTint="@null" />
<LinearLayout
android:id="@+id/tasksContainer"
android:id="@+id/layoutTasksSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:orientation="vertical" />
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="Tarefas do Dia"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvTasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:nestedScrollingEnabled="false" />
</LinearLayout>
<TextView
android:id="@+id/tvNoTasksIncentive"
@@ -398,19 +418,6 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 8. BOTÃO ADICIONAR TAREFA -->
<Button
android:id="@+id/btnAddTasks"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginBottom="32dp"
android:background="@drawable/button_primary"
android:text="+ Adicionar Tarefa"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:backgroundTint="@null" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -9,7 +9,7 @@
android:layout_height="wrap_content"
android:hint="Search..." />
<androidx.recyclerview.widget.RecyclerView
<ListView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -11,7 +11,7 @@
<item name="colorOnSecondary">@color/background_light</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">@color/background_light</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
<item name="android:windowLightStatusBar" tools:targetApi="M">false</item>
<!-- Customize your theme here. -->
<item name="android:windowBackground">@color/background_light</item>
</style>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="nodeCircle" type="id">circle</item>
<item name="nodeDayInitial" type="id">dayInitial</item>
<item name="nodeDayLabel" type="id">nodeDayLabel</item>
<item name="nodeProgress" type="id">nodeProgress</item>
<item name="nodeCircle" type="id" />
<item name="nodeDayInitial" type="id" />
<item name="nodeDayLabel" type="id" />
<item name="nodeProgress" type="id" />
</resources>

View File

@@ -11,7 +11,7 @@
<item name="colorOnSecondary">#11181C</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">#F9FAFB</item>
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
<item name="android:windowLightStatusBar" tools:targetApi="M">true</item>
<!-- Customize your theme here. -->
<item name="android:windowBackground">#F9FAFB</item>
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,3 @@
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false