continuar utilizador

This commit is contained in:
MeuNome
2026-04-23 17:08:39 +01:00
parent 7c05b71152
commit 4c14dc5bf2
38 changed files with 1736 additions and 72 deletions

View File

@@ -28,6 +28,7 @@
<activity android:name=".SettingsActivity" /> <activity android:name=".SettingsActivity" />
<activity android:name=".StreakActivity" /> <activity android:name=".StreakActivity" />
<activity android:name=".FindFriendsActivity" /> <activity android:name=".FindFriendsActivity" />
<activity android:name=".TrophiesActivity" />

View File

@@ -1,14 +1,32 @@
package com.fluxup.app; package com.fluxup.app;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseAuthException;
import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.FirebaseUser;
/**
* AuthManager - Classe responsável pela gestão centralizada da autenticação Firebase.
* Segue padrões de desenvolvimento sénior para garantir modularidade e tratamento de erros robusto.
*/
public class AuthManager { public class AuthManager {
private static AuthManager instance;
private AuthManager() { private static AuthManager instance;
// private constructor private final FirebaseAuth mAuth;
// Interface para comunicação de resultados com a UI (Activities/Fragments)
public interface AuthCallback {
void onSuccess(FirebaseUser user);
void onError(String errorMessage);
} }
// Construtor privado para padrão Singleton
private AuthManager() {
mAuth = FirebaseAuth.getInstance();
}
// Método para obter a instância única do AuthManager
public static synchronized AuthManager getInstance() { public static synchronized AuthManager getInstance() {
if (instance == null) { if (instance == null) {
instance = new AuthManager(); instance = new AuthManager();
@@ -16,22 +34,79 @@ public class AuthManager {
return instance; return instance;
} }
/**
* Verifica se existe um utilizador autenticado na sessão atual.
* @return FirebaseUser ou null se não houver sessão ativa.
*/
public FirebaseUser getCurrentUser() { public FirebaseUser getCurrentUser() {
return null; return mAuth.getCurrentUser();
}
public void loginUtilizador(String email, String password, AuthCallback callback) {
// Mock implementation
callback.onSuccess();
} }
/**
* Realiza o registo de um novo utilizador.
* Inclui tratamento de erros específico para passwords fracas e emails inválidos.
*/
public void registrarUtilizador(String email, String password, AuthCallback callback) { public void registrarUtilizador(String email, String password, AuthCallback callback) {
// Mock implementation mAuth.createUserWithEmailAndPassword(email, password)
callback.onSuccess(); .addOnCompleteListener(task -> {
if (task.isSuccessful()) {
callback.onSuccess(mAuth.getCurrentUser());
} else {
callback.onError(handleAuthError(task.getException()));
}
});
} }
public interface AuthCallback { /**
void onSuccess(); * Realiza o login de um utilizador existente.
void onFailure(Exception e); * A gestão da sessão é feita automaticamente pelo Firebase (Persistence).
*/
public void loginUtilizador(String email, String password, AuthCallback callback) {
mAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
callback.onSuccess(mAuth.getCurrentUser());
} else {
callback.onError(handleAuthError(task.getException()));
}
});
}
/**
* Encerra a sessão do utilizador atual.
*/
public void logout() {
mAuth.signOut();
}
/**
* Traduz exceções do Firebase Auth em mensagens amigáveis para o utilizador final.
*/
private String handleAuthError(Exception e) {
if (e instanceof FirebaseAuthException) {
String errorCode = ((FirebaseAuthException) e).getErrorCode();
switch (errorCode) {
case "ERROR_INVALID_EMAIL":
return "O formato do email é inválido.";
case "ERROR_WEAK_PASSWORD":
return "A palavra-passe é demasiado fraca. Use pelo menos 6 caracteres.";
case "ERROR_USER_NOT_FOUND":
return "Não existe nenhum utilizador registado com este email.";
case "ERROR_WRONG_PASSWORD":
return "Palavra-passe incorreta.";
case "ERROR_EMAIL_ALREADY_IN_USE":
return "Este email já está a ser utilizado por outra conta.";
case "ERROR_USER_DISABLED":
return "Esta conta foi desativada.";
case "ERROR_TOO_MANY_REQUESTS":
return "Demasiadas tentativas falhadas. Tente mais tarde.";
case "ERROR_OPERATION_NOT_ALLOWED":
return "O login com email/password não está ativado no Firebase.";
default:
return "Erro na autenticação: " + e.getLocalizedMessage();
}
}
return e != null ? e.getLocalizedMessage() : "Ocorreu um erro desconhecido.";
} }
} }

View File

@@ -0,0 +1,114 @@
package com.fluxup.app;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.button.MaterialButton;
import java.util.ArrayList;
import java.util.List;
public class FindFriendsActivity extends AppCompatActivity {
private ImageButton btnClose;
private RecyclerView rvSuggestions;
private View btnContacts, btnSearchByName, btnProfileLink;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_find_friends);
btnClose = findViewById(R.id.btnClose);
rvSuggestions = findViewById(R.id.rvSuggestions);
btnContacts = findViewById(R.id.btnContacts);
btnSearchByName = findViewById(R.id.btnSearchByName);
btnProfileLink = findViewById(R.id.btnProfileLink);
btnClose.setOnClickListener(v -> finish());
setupSuggestions();
}
private void setupSuggestions() {
rvSuggestions.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
List<Suggestion> suggestions = new ArrayList<>();
suggestions.add(new Suggestion("Maria Silva", "Segue você", R.color.reward_yellow));
suggestions.add(new Suggestion("João Pereira", "Amigo em comum", R.color.success_green));
suggestions.add(new Suggestion("Ana Costa", "Segue você", R.color.streak_orange));
suggestions.add(new Suggestion("Ricardo M.", "Novo no Fluxup", R.color.streak_blue));
SuggestionsAdapter adapter = new SuggestionsAdapter(suggestions);
rvSuggestions.setAdapter(adapter);
}
// --- Data Model ---
private static class Suggestion {
String name;
String info;
int colorRes;
Suggestion(String name, String info, int colorRes) {
this.name = name;
this.info = info;
this.colorRes = colorRes;
}
}
// --- Adapter ---
private class SuggestionsAdapter extends RecyclerView.Adapter<SuggestionsAdapter.ViewHolder> {
private final List<Suggestion> suggestions;
SuggestionsAdapter(List<Suggestion> suggestions) {
this.suggestions = suggestions;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_friend_suggestion, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Suggestion item = suggestions.get(position);
holder.tvName.setText(item.name);
holder.tvInfo.setText(item.info);
holder.ivAvatar.setColorFilter(getResources().getColor(item.colorRes));
holder.btnFollow.setOnClickListener(v -> {
holder.btnFollow.setText("Seguindo");
holder.btnFollow.setEnabled(false);
holder.btnFollow.setAlpha(0.6f);
});
}
@Override
public int getItemCount() {
return suggestions.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
ImageView ivAvatar;
TextView tvName, tvInfo;
MaterialButton btnFollow;
ViewHolder(View view) {
super(view);
ivAvatar = view.findViewById(R.id.ivFriendAvatar);
tvName = view.findViewById(R.id.tvFriendName);
tvInfo = view.findViewById(R.id.tvFriendInfo);
btnFollow = view.findViewById(R.id.btnFollowBack);
}
}
}
}

View File

@@ -0,0 +1,79 @@
package com.fluxup.app;
import com.google.firebase.firestore.FirebaseFirestore;
import com.google.firebase.firestore.ListenerRegistration;
import com.google.firebase.firestore.QueryDocumentSnapshot;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class FirestoreManager {
private static FirestoreManager instance;
private final FirebaseFirestore db;
private FirestoreManager() {
db = FirebaseFirestore.getInstance();
}
public static synchronized FirestoreManager getInstance() {
if (instance == null) {
instance = new FirestoreManager();
}
return instance;
}
/**
* Observa as mudanças no documento do utilizador em tempo real.
*/
public ListenerRegistration observeUser(String uid, Consumer<Usuario> listener) {
return db.collection("users").document(uid)
.addSnapshotListener((snapshot, e) -> {
if (e != null) return;
if (snapshot != null && snapshot.exists()) {
Usuario usuario = snapshot.toObject(Usuario.class);
if (usuario != null) listener.accept(usuario);
}
});
}
/**
* Observa as tarefas do utilizador em tempo real.
*/
public ListenerRegistration observeTasks(String uid, Consumer<List<Task>> listener) {
return db.collection("tasks")
.whereEqualTo("userId", uid)
.addSnapshotListener((snapshots, e) -> {
if (e != null) return;
List<Task> tasks = new ArrayList<>();
if (snapshots != null) {
for (QueryDocumentSnapshot doc : snapshots) {
tasks.add(doc.toObject(Task.class));
}
}
listener.accept(tasks);
});
}
/**
* Atualiza o estado de uma tarefa.
*/
public void updateTask(Task task) {
db.collection("tasks").document(task.id).set(task);
}
/**
* Adiciona uma nova tarefa.
*/
public void addTask(Task task) {
db.collection("tasks").document(task.id).set(task);
}
/**
* Atualiza campos específicos do perfil do utilizador (ex: XP, Streak).
*/
public void updateUserStats(String uid, Map<String, Object> updates) {
db.collection("users").document(uid).update(updates);
}
}

View File

@@ -0,0 +1,59 @@
package com.fluxup.app;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.appcompat.app.AppCompatDelegate;
import com.fluxup.app.BuildConfig;
import com.google.firebase.FirebaseApp;
import com.google.firebase.appcheck.FirebaseAppCheck;
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory;
import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory;
public class FluxupApplication extends Application {
private static final String TAG = "FLUXUP_DEBUG";
@Override
public void onCreate() {
super.onCreate();
// Aplicar preferências de tema
SharedPreferences prefs = getSharedPreferences("FluxupSettings", Context.MODE_PRIVATE);
if (prefs.getBoolean("darkMode", false)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
} else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
// Inicializar Firebase
FirebaseApp.initializeApp(this);
Log.d(TAG, "FirebaseApp inicializado com sucesso.");
// Inicializar Firebase App Check
FirebaseAppCheck firebaseAppCheck = FirebaseAppCheck.getInstance();
if (BuildConfig.DEBUG) {
// 1. Instala o provider
firebaseAppCheck.installAppCheckProviderFactory(DebugAppCheckProviderFactory.getInstance());
// 2. FORÇAR a exibição do token no Logcat
// Adicionamos um log específico com a tag "FLUXUP_DEBUG"
Log.d("FLUXUP_DEBUG", "--------------------------------------------------");
Log.d("FLUXUP_DEBUG", "COPIE ESTE TOKEN PARA A CONSOLA FIREBASE:");
Log.d("FLUXUP_DEBUG", "Debug Token: " + "Procure pela mensagem acima ou no log do sistema");
Log.d("FLUXUP_DEBUG", "--------------------------------------------------");
} else {
// Em modo RELEASE, usamos o Play Integrity (recomendado para Android).
firebaseAppCheck.installAppCheckProviderFactory(
PlayIntegrityAppCheckProviderFactory.getInstance());
Log.d(TAG, "App Check: Play Integrity Provider instalado.");
}
// Garantir que os tokens são renovados automaticamente e usamos sempre o mais recente
firebaseAppCheck.setTokenAutoRefreshEnabled(true);
Log.d(TAG, "App Check: Auto-refresh de tokens ativado.");
}
}

View File

@@ -79,9 +79,7 @@ public class InicioFragment extends Fragment {
}); });
} }
view.findViewById(R.id.btnAddTasks).setOnClickListener(v -> { view.findViewById(R.id.btnAddTasks).setOnClickListener(v -> showAddTaskDialog());
Toast.makeText(getContext(), "Adicionar tarefas: Implementação futura", Toast.LENGTH_SHORT).show();
});
View btnStreak = view.findViewById(R.id.btnStreak); View btnStreak = view.findViewById(R.id.btnStreak);
if (btnStreak != null) { if (btnStreak != null) {
@@ -331,4 +329,76 @@ public class InicioFragment extends Fragment {
} }
pauseTimer(); // Parar o timer se a view for destruída pauseTimer(); // Parar o timer se a view for destruída
} }
private void showAddTaskDialog() {
if (getContext() == null) return;
// Build input field
android.widget.EditText editText = new android.widget.EditText(getContext());
editText.setHint("Escreve o teu desafio…");
editText.setInputType(android.text.InputType.TYPE_CLASS_TEXT
| android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
editText.setTextSize(16);
editText.setSingleLine(false);
editText.setMaxLines(3);
// Wrap with padding
android.widget.FrameLayout container = new android.widget.FrameLayout(getContext());
android.widget.FrameLayout.LayoutParams lp = new android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT);
int dp24 = (int) (24 * getResources().getDisplayMetrics().density);
lp.setMargins(dp24, dp24 / 2, dp24, 0);
editText.setLayoutParams(lp);
container.addView(editText);
android.app.AlertDialog dialog = new com.google.android.material.dialog.MaterialAlertDialogBuilder(getContext())
.setTitle("Novo Desafio")
.setView(container)
.setNegativeButton("Cancelar", null)
.setPositiveButton("Adicionar", null)
dialog.setOnShowListener(d -> {
// Style buttons
android.widget.Button btnAdd = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE);
android.widget.Button btnCancel = dialog.getButton(android.app.AlertDialog.BUTTON_NEGATIVE);
if (btnAdd != null) btnAdd.setTextColor(getResources().getColor(R.color.primary_purple));
if (btnCancel != null) btnCancel.setTextColor(getResources().getColor(R.color.text_secondary));
// Override positive so it validates before dismissing
if (btnAdd != null) {
btnAdd.setOnClickListener(v -> {
String title = editText.getText().toString().trim();
if (title.isEmpty()) {
editText.setError("Escreve um desafio");
return;
}
saveNewTask(title);
dialog.dismiss();
});
}
editText.requestFocus();
if (dialog.getWindow() != null) {
dialog.getWindow().setSoftInputMode(
android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
});
dialog.show();
}
jhvjhvkvjhv
private void saveNewTask(String title) {
com.google.firebase.auth.FirebaseUser currentUser = AuthManager.getInstance().getCurrentUser();
if (currentUser == null) return;
String uid = currentUser.getUid();
String taskId = com.google.firebase.firestore.FirebaseFirestore.getInstance()
.collection("tasks").document().getId();
Task task = new Task(taskId, title, 10, uid);
FirestoreManager.getInstance().addTask(task);
}
} }

View File

@@ -45,6 +45,9 @@ public class MainActivity extends AppCompatActivity {
if (itemId == R.id.nav_inicio) { if (itemId == R.id.nav_inicio) {
selectedFragment = new InicioFragment(); selectedFragment = new InicioFragment();
} else if (itemId == R.id.nav_trophies) {
startActivity(new android.content.Intent(this, TrophiesActivity.class));
return true;
} else if (itemId == R.id.nav_profile) { } else if (itemId == R.id.nav_profile) {
selectedFragment = new ProfileFragment(); selectedFragment = new ProfileFragment();
} else if (itemId == R.id.nav_search) { } else if (itemId == R.id.nav_search) {

View File

@@ -43,6 +43,11 @@ public class ProfileFragment extends Fragment {
tvTotalXP = view.findViewById(R.id.tvTotalXP); tvTotalXP = view.findViewById(R.id.tvTotalXP);
tvLeagueName = view.findViewById(R.id.tvLeagueName); tvLeagueName = view.findViewById(R.id.tvLeagueName);
tvAchievementsCount = view.findViewById(R.id.tvAchievementsCount); tvAchievementsCount = view.findViewById(R.id.tvAchievementsCount);
View cardLeague = view.findViewById(R.id.cardLeague);
cardLeague.setOnClickListener(v -> {
Intent intent = new Intent(getActivity(), TrophiesActivity.class);
startActivity(intent);
});
startObservingUser(); startObservingUser();

View File

@@ -1,12 +1,159 @@
package com.fluxup.app; package com.fluxup.app;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
public class StreakActivity extends AppCompatActivity { public class StreakActivity extends AppCompatActivity {
private RecyclerView rvCalendar;
private TextView tvStreakCount;
private ImageButton btnClose;
private TabLayout tabLayout;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_streak); setContentView(R.layout.activity_streak);
rvCalendar = findViewById(R.id.rvCalendar);
tvStreakCount = findViewById(R.id.tvStreakCount);
btnClose = findViewById(R.id.btnClose);
tabLayout = findViewById(R.id.tabLayout);
btnClose.setOnClickListener(v -> finish());
setupCalendar();
setupTabs();
}
private void setupTabs() {
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
// Future: toggle between personal and friends stats
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {}
});
}
private void setupCalendar() {
rvCalendar.setLayoutManager(new GridLayoutManager(this, 7));
List<CalendarDay> days = new ArrayList<>();
// Simulating April 2026 (Starts on a Wednesday)
// Add empty slots for Mon, Tue
days.add(new CalendarDay(0, false, false)); // Mon
days.add(new CalendarDay(0, false, false)); // Tue
// Days 1 to 30
for (int i = 1; i <= 30; i++) {
boolean isActive = (i >= 1 && i <= 7); // 7-day streak for demo
boolean isCurrent = (i == 7);
days.add(new CalendarDay(i, isActive, isCurrent));
}
CalendarAdapter adapter = new CalendarAdapter(days);
rvCalendar.setAdapter(adapter);
}
// --- Inner Models & Adapter ---
private static class CalendarDay {
int dayNumber;
boolean isActive;
boolean isCurrent;
CalendarDay(int dayNumber, boolean isActive, boolean isCurrent) {
this.dayNumber = dayNumber;
this.isActive = isActive;
this.isCurrent = isCurrent;
}
}
private class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.ViewHolder> {
private final List<CalendarDay> days;
CalendarAdapter(List<CalendarDay> days) {
this.days = days;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_calendar_day, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
CalendarDay day = days.get(position);
if (day.dayNumber == 0) {
holder.tvDayNumber.setText("");
holder.dayBackground.setVisibility(View.GONE);
holder.streakConnector.setVisibility(View.GONE);
} else {
holder.tvDayNumber.setText(String.valueOf(day.dayNumber));
if (day.isActive) {
holder.dayBackground.setVisibility(View.VISIBLE);
holder.tvDayNumber.setTextColor(getResources().getColor(R.color.white));
// Simple logic for streak connection: if previous day was also active
if (position > 0 && days.get(position-1).isActive && day.dayNumber > 1) {
holder.streakConnector.setVisibility(View.VISIBLE);
} else {
holder.streakConnector.setVisibility(View.GONE);
}
} else {
holder.dayBackground.setVisibility(View.GONE);
holder.streakConnector.setVisibility(View.GONE);
holder.tvDayNumber.setTextColor(getResources().getColor(R.color.text_primary));
}
if (day.isCurrent) {
holder.dayIndicator.setVisibility(View.VISIBLE);
} else {
holder.dayIndicator.setVisibility(View.GONE);
}
}
}
@Override
public int getItemCount() {
return days.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
TextView tvDayNumber;
View dayBackground;
View streakConnector;
View dayIndicator;
ViewHolder(View view) {
super(view);
tvDayNumber = view.findViewById(R.id.tvDayNumber);
dayBackground = view.findViewById(R.id.dayBackground);
streakConnector = view.findViewById(R.id.streakConnector);
dayIndicator = view.findViewById(R.id.dayIndicator);
}
}
} }
} }

View File

@@ -0,0 +1,19 @@
package com.fluxup.app;
public class Task {
public String id;
public String title;
public boolean completed;
public int xpReward;
public String userId;
public Task() {}
public Task(String id, String title, int xpReward, String userId) {
this.id = id;
this.title = title;
this.completed = false;
this.xpReward = xpReward;
this.userId = userId;
}
}

View File

@@ -0,0 +1,145 @@
package com.fluxup.app;
import android.os.Bundle;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.firestore.ListenerRegistration;
public class TrophiesActivity extends AppCompatActivity {
private TextView tvDivisionTitle, tvTimeRemaining, tvTrophyProgress, tvMotivational;
private ImageButton btnBack;
private LinearLayout trophyContainer, inactiveState;
private ImageView trophy1, trophy2, trophy3;
private FirestoreManager firestoreManager;
private FirebaseAuth mAuth;
private ListenerRegistration userListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trophies);
firestoreManager = FirestoreManager.getInstance();
mAuth = FirebaseAuth.getInstance();
initViews();
setupListeners();
observeUserData();
}
private void initViews() {
tvDivisionTitle = findViewById(R.id.tvDivisionTitle);
tvTimeRemaining = findViewById(R.id.tvTimeRemaining);
tvTrophyProgress = findViewById(R.id.tvTrophyProgress);
tvMotivational = findViewById(R.id.tvMotivational);
btnBack = findViewById(R.id.btnBack);
trophyContainer = findViewById(R.id.trophyContainer);
inactiveState = findViewById(R.id.inactiveState);
trophy1 = findViewById(R.id.trophy1);
trophy2 = findViewById(R.id.trophy2);
trophy3 = findViewById(R.id.trophy3);
}
private void setupListeners() {
btnBack.setOnClickListener(v -> finish());
}
private void observeUserData() {
String uid = mAuth.getUid();
if (uid != null) {
userListener = firestoreManager.observeUser(uid, this::updateUI);
}
}
private void updateUI(Usuario user) {
if (user == null) return;
tvDivisionTitle.setText("Divisão " + user.league);
// Trophy logic based on streak
int currentStreak = user.streak;
int trophies = user.trophiesCount;
// Update trophy visuals
updateTrophyIcons(trophies);
// Progress message
int daysToNext = 30 - (currentStreak % 30);
if (daysToNext == 30 && currentStreak > 0) {
tvTrophyProgress.setText("Troféu conquistado! Mantém a ofensiva.");
} else {
tvTrophyProgress.setText("Faltam " + daysToNext + " dias para o próximo troféu");
}
// Handle inactive state
if (currentStreak == 0) {
inactiveState.setVisibility(View.VISIBLE);
trophyContainer.setAlpha(0.5f);
tvMotivational.setVisibility(View.GONE);
} else {
inactiveState.setVisibility(View.GONE);
trophyContainer.setAlpha(1.0f);
tvMotivational.setVisibility(View.VISIBLE);
tvMotivational.setText("Estás a progredir bem na Divisão " + user.league + "!");
}
// Simple scale animation for the active trophy
animateActiveTrophy(trophies);
}
private void updateTrophyIcons(int count) {
// Reset alphas
trophy1.setAlpha(0.3f);
trophy2.setAlpha(0.3f);
trophy3.setAlpha(0.3f);
if (count >= 1) trophy1.setAlpha(1.0f);
if (count >= 2) trophy2.setAlpha(1.0f);
if (count >= 3) trophy3.setAlpha(1.0f);
// Highlight current progress (the next one)
if (count == 0) highlightTrophy(trophy1);
else if (count == 1) highlightTrophy(trophy2);
else if (count == 2) highlightTrophy(trophy3);
}
private void highlightTrophy(ImageView trophy) {
trophy.setAlpha(0.6f);
trophy.setBackgroundResource(R.drawable.circle_bg);
trophy.setBackgroundTintList(android.content.res.ColorStateList.valueOf(getResources().getColor(R.color.primary_purple)));
trophy.setPadding(12, 12, 12, 12);
}
private void animateActiveTrophy(int count) {
ImageView target = null;
if (count == 0) target = trophy1;
else if (count == 1) target = trophy2;
else if (count == 2) target = trophy3;
if (target != null) {
ScaleAnimation scale = new ScaleAnimation(1f, 1.1f, 1f, 1.1f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
scale.setDuration(1000);
scale.setRepeatCount(Animation.INFINITE);
scale.setRepeatMode(Animation.REVERSE);
target.startAnimation(scale);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (userListener != null) {
userListener.remove();
}
}
}

View File

@@ -17,6 +17,7 @@ public class Usuario {
public String handle = ""; public String handle = "";
public String bio = ""; public String bio = "";
public int achievementsCount = 0; public int achievementsCount = 0;
public int trophiesCount = 0;
public Usuario() {} public Usuario() {}

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:color="#40FFFFFF">
<solid android:color="#CCCCCC" /> <item>
</shape> <shape android:shape="rectangle">
<solid android:color="@color/primary_purple" />
<corners android:radius="@dimen/radius_duo" />
</shape>
</item>
</ripple>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"> android:shape="rectangle">
<solid android:color="#FFFFFF" /> <solid android:color="@color/white" />
<corners android:radius="8dp" /> <corners android:radius="@dimen/radius_duo" />
<stroke android:width="2dp" android:color="@color/border_color" />
</shape> </shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/text_primary"
android:pathData="M19,6.41L17.59,5,12,10.59,6.41,5,5,6.41,10.59,12,5,17.59,6.41,19,12,13.41,17.59,19,19,17.59,13.41,12,19,6.41z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M20,4H4C2.9,4 2.01,4.9 2.01,6L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6C22,4.9 21.1,4 20,4zM4,6h16v7H4V6zM19,18H5l4,-5h3l1,1l2,-3l4,7z"/>
</vector>

View File

@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:viewportWidth="24.0" android:viewportWidth="24"
android:viewportHeight="24.0"> android:viewportHeight="24">
<path <path
android:fillColor="#FF000000" android:fillColor="@color/streak_orange"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" /> android:pathData="M13.5,0.67C13.5,0.67 14.92,3.42 14.92,5.31C14.92,7.34 13.78,8.23 12.33,9.72L12.33,4.01C12.33,4.01 10.97,1.69 8.01,3.42C5.05,5.15 6.09,10.22 6.09,10.22C6.09,10.22 4.19,7.67 2,12.31C-0.19,16.95 2.13,21.82 2.13,21.82C2.13,21.82 4.1,23.33 8.35,23.33C12.6,23.33 14,21 15.35,23.33C16.7,25.66 19.3,23.33 20,22C20.7,20.67 22,17.33 22,14.67C22,12 18,10 18,10C18,10 20.35,11.38 20.35,13.62C20.35,15.86 19.31,16.5 17.5,14.67C15.69,12.84 16,9 16,9C16,9 18.66,10 18.66,7C18.66,4 14,0.67 13.5,0.67Z" />
</vector> </vector>

View File

@@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#000000"
android:strokeWidth="2"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M18,2H6A2,2 0,0 0,4 4V7C4,8.11 4.9,9 6,9H7V11C7,12.71 8.06,14.2 9.5,14.8V17H8V19H16V17H14.5V14.8C15.94,14.2 17,12.71 17,11V9H18A2,2 0,0 0,20 7V4A2,2 0,0 0,18 2Z" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#000000"
android:strokeWidth="2"
android:pathData="M6,7V4H7V7" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="#000000"
android:strokeWidth="2"
android:pathData="M17,4V7H18" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81c1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3s-3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65c0,1.61 1.31,2.92 2.92,2.92c1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="200dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#B39DDB"
android:pathData="M12,2A10,10 0,0,0 2,12A10,10 0,0,0 12,22A10,10 0,0,0 22,12A10,10 0,0,0 12,2M12,4A8,8 0,0,1 20,12A8,8 0,0,1 12,20A8,8 0,0,1 4,12A8,8 0,0,1 12,4M8,9.5L10,11.5L8,13.5L9.5,15L11.5,13L13.5,15L15,13.5L13,11.5L15,9.5L13.5,8L11.5,10L9.5,8L8,9.5Z" />
<path
android:fillColor="#7E57C2"
android:pathData="M17,5V7H19V5H17M14,2V4H16V2H14M20,8V10H22V8H20Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#CD7F32"
android:pathData="M18,2H6A2,2 0,0 0,4 4V7C4,8.11 4.9,9 6,9H7V11C7,12.71 8.06,14.2 9.5,14.8V17H8V19H16V17H14.5V14.8C15.94,14.2 17,12.71 17,11V9H18A2,2 0,0 0,20 7V4A2,2 0,0 0,18 2M6,7V4H7V7H6M18,7H17V4H18V7Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFD700"
android:pathData="M18,2H6A2,2 0,0 0,4 4V7C4,8.11 4.9,9 6,9H7V11C7,12.71 8.06,14.2 9.5,14.8V17H8V19H16V17H14.5V14.8C15.94,14.2 17,12.71 17,11V9H18A2,2 0,0 0,20 7V4A2,2 0,0 0,18 2M6,7V4H7V7H6M18,7H17V4H18V7Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#C0C0C0"
android:pathData="M18,2H6A2,2 0,0 0,4 4V7C4,8.11 4.9,9 6,9H7V11C7,12.71 8.06,14.2 9.5,14.8V17H8V19H16V17H14.5V14.8C15.94,14.2 17,12.71 17,11V9H18A2,2 0,0 0,20 7V4A2,2 0,0 0,18 2M6,7V4H7V7H6M18,7H17V4H18V7Z" />
</vector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/white" />
<stroke
android:width="2dp"
android:color="@color/border_color" />
</shape>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape
android:innerRadiusRatio="1.5"
android:shape="ring"
android:thickness="4dp"
android:useLevel="false">
<solid android:color="@color/border_color" />
</shape>
</item>
<item android:id="@android:id/progress">
<rotate
android:fromDegrees="270"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="270">
<shape
android:innerRadiusRatio="1.5"
android:shape="ring"
android:thickness="4dp"
android:useLevel="true">
<solid android:color="@color/primary_purple" />
</shape>
</rotate>
</item>
</layer-list>

View File

@@ -1,5 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
android:shape="rectangle"> <item android:id="@android:id/background">
<solid android:color="#CCCCCC" /> <shape>
</shape> <corners android:radius="12dp" />
<solid android:color="@color/border_color" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="12dp" />
<solid android:color="@color/success_green" />
</shape>
</clip>
</item>
</layer-list>

View File

@@ -1,5 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"> android:shape="oval">
<solid android:color="#CCCCCC" /> <solid android:color="@color/white" />
<stroke
android:width="4dp"
android:color="@color/primary_purple" />
<size
android:width="200dp"
android:height="200dp" />
</shape> </shape>

View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">
<!-- Header -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingHorizontal="16dp">
<ImageButton
android:id="@+id/btnClose"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
app:tint="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Encontre os seus amigos"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
</RelativeLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- Main Options Cards -->
<androidx.cardview.widget.CardView
android:id="@+id/btnContacts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="20dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/card_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_contacts"
app:tint="@color/primary_purple" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Escolher nos contactos"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/btnSearchByName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="20dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/card_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_search"
app:tint="@color/streak_blue" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Buscar por nome"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/btnProfileLink"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:cardCornerRadius="20dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/card_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_share"
app:tint="@color/success_green" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="Link do seu perfil"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Suggestions Section -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sugestões de amigos"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnViewAll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:text="VER TODOS"
android:textColor="@color/primary_purple"
android:textSize="12sp"
android:textStyle="bold" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSuggestions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@@ -1,12 +1,251 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout 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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:background="@color/background_light">
<TextView <com.google.android.material.appbar.AppBarLayout
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Streak Activity" /> android:background="@color/card_background"
app:elevation="0dp">
</LinearLayout> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingHorizontal="16dp">
<ImageButton
android:id="@+id/btnClose"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
app:tint="@color/text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Ofensiva"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabIndicatorColor="@color/primary_purple"
app:tabIndicatorHeight="3dp"
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="Pessoal" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Amigos" />
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<!-- Streak Main State -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:layout_marginBottom="32dp">
<ImageView
android:id="@+id/ivBigFlame"
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_flame"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/tvStreakCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="7 dias de ofensiva!"
android:textColor="@color/text_primary"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Estás imparável! Continua assim."
android:textColor="@color/text_secondary"
android:textSize="16sp"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- Calendar Section -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="@dimen/radius_duo"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvMonthName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="abril de 2026"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true">
<ImageButton
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_back"
app:tint="@color/text_secondary" />
<ImageButton
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:rotation="180"
android:src="@drawable/ic_back"
app:tint="@color/text_secondary" />
</LinearLayout>
</RelativeLayout>
<!-- Days of Week Headers -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="S" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="T" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Q" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="Q" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="S" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="S" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
<TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="D" android:textAlignment="center" android:textStyle="bold" android:textColor="@color/text_secondary" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvCalendar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Statistics Grid -->
<GridLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2">
<!-- Days of Practice Card -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dias de prática"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="12"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Focus Sessions Card -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_columnWeight="1"
android:layout_marginStart="8dp"
android:layout_marginBottom="16dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sessões de foco"
android:textColor="@color/text_secondary"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="24"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="4dp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</GridLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/card_background"
app:elevation="0dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="64dp"
android:paddingHorizontal="16dp">
<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" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Troféus"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
</RelativeLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<!-- Division Info -->
<TextView
android:id="@+id/tvDivisionTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Divisão Prata"
android:textColor="@color/primary_purple"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTimeRemaining"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="3 dias restantes"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
<!-- Trophy Progression Container -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
app:cardCornerRadius="@dimen/radius_duo"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="32dp">
<LinearLayout
android:id="@+id/trophyContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<!-- Trophies will be added here dynamically or statically -->
<ImageView
android:id="@+id/trophy1"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="8dp"
android:src="@drawable/ic_trophy_bronze" />
<ImageView
android:id="@+id/trophy2"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="8dp"
android:src="@drawable/ic_trophy_silver"
android:padding="4dp"
android:background="@drawable/circle_bg"
android:backgroundTint="#1A6200EE" />
<ImageView
android:id="@+id/trophy3"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="8dp"
android:alpha="0.3"
android:src="@drawable/ic_trophy_gold" />
</LinearLayout>
<TextView
android:id="@+id/tvTrophyProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Faltam 15 dias para o próximo troféu"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Inactive State (Initially Hidden) -->
<LinearLayout
android:id="@+id/inactiveState"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:src="@drawable/ic_sleeping_char" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Hora de voltar!"
android:textColor="@color/text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Completa tarefas para competir esta semana."
android:textAlignment="center"
android:textColor="@color/text_secondary"
android:textSize="14sp" />
</LinearLayout>
<!-- Active Message -->
<TextView
android:id="@+id/tvMotivational"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Estás a progredir bem na Divisão Prata!"
android:textAlignment="center"
android:textColor="@color/text_secondary"
android:textSize="16sp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -170,13 +170,17 @@
<!-- League Card --> <!-- League Card -->
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:id="@+id/cardLeague"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_columnWeight="1" android:layout_columnWeight="1"
android:layout_margin="6dp" android:layout_margin="6dp"
app:cardCornerRadius="@dimen/radius_duo" app:cardCornerRadius="@dimen/radius_duo"
app:cardElevation="2dp" app:cardElevation="2dp"
app:contentPadding="16dp"> app:contentPadding="16dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">
<!-- Streak Background (Line connecting days) -->
<View
android:id="@+id/streakConnector"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="@color/streak_blue"
android:visibility="gone" />
<!-- Active Day Background (Circle) -->
<View
android:id="@+id/dayBackground"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:background="@drawable/circle_bg"
android:backgroundTint="@color/streak_blue"
android:visibility="gone" />
<!-- Day Number -->
<TextView
android:id="@+id/tvDayNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="1"
android:textColor="@color/text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<!-- Indicator Dot (Optional) -->
<View
android:id="@+id/dayIndicator"
android:layout_width="4dp"
android:layout_height="4dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="8dp"
android:background="@drawable/circle_bg"
android:backgroundTint="@color/primary_purple"
android:visibility="gone" />
</FrameLayout>

View File

@@ -1,41 +1,67 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/dayOfMonth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1"
android:textColor="@android:color/black"
android:textSize="16sp" />
<TextView
android:id="@+id/dayOfWeek"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mon"
android:textColor="@android:color/darker_gray"
android:textSize="12sp" />
<View <View
android:id="@+id/topConnector" android:id="@+id/topConnector"
android:layout_width="2dp" android:layout_width="4dp"
android:layout_height="16dp" android:layout_height="20dp"
android:background="@android:color/darker_gray" /> android:background="@color/border_color" />
<View <FrameLayout
android:id="@+id/node" android:id="@+id/nodeContainer"
android:layout_width="16dp" android:layout_width="80dp"
android:layout_height="16dp" /> android:layout_height="80dp">
<!-- Progress Ring (Outer) -->
<ProgressBar
android:id="@+id/nodeProgress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/node_progress_ring" />
<!-- Base Circle (Inner) -->
<View
android:id="@+id/nodeCircle"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_gravity="center"
android:background="@drawable/node_circle_bg" />
<!-- Icon or Initial (Optional) -->
<TextView
android:id="@+id/nodeDayInitial"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="S"
android:textColor="@color/text_secondary"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<TextView
android:id="@+id/nodeDayLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="SEG"
android:textColor="@color/text_secondary"
android:textSize="12sp"
android:textStyle="bold" />
<View <View
android:id="@+id/bottomConnector" android:id="@+id/bottomConnector"
android:layout_width="2dp" android:layout_width="4dp"
android:layout_height="16dp" android:layout_height="20dp"
android:background="@android:color/darker_gray" /> android:background="@color/border_color" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="24dp"
app:cardElevation="2dp"
app:cardBackgroundColor="@color/card_background">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<androidx.cardview.widget.CardView
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="32dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/ivFriendAvatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/ic_nav_profile"
app:tint="@color/primary_purple" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tvFriendName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome"
android:textColor="@color/text_primary"
android:textSize="14sp"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end" />
<TextView
android:id="@+id/tvFriendInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Segue você"
android:textColor="@color/text_secondary"
android:textSize="11sp"
android:layout_marginBottom="12dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFollowBack"
android:layout_width="match_parent"
android:layout_height="36dp"
android:text="Seguir"
android:textAllCaps="false"
android:textColor="@color/white"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="18dp"
android:padding="0dp"
android:insetTop="0dp"
android:insetBottom="0dp"
app:backgroundTint="@color/primary_purple" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -4,6 +4,10 @@
android:id="@+id/nav_inicio" android:id="@+id/nav_inicio"
android:icon="@drawable/ic_nav_home" android:icon="@drawable/ic_nav_home"
android:title="Início" /> android:title="Início" />
<item
android:id="@+id/nav_trophies"
android:icon="@drawable/ic_nav_trophy"
android:title="Troféus" />
<item <item
android:id="@+id/nav_profile" android:id="@+id/nav_profile"
android:icon="@drawable/ic_nav_profile" android:icon="@drawable/ic_nav_profile"

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Dark Mode Palette -->
<!-- DO NOT override white/black directly if they are used for text on primary colors -->
<color name="background_light">#0F172A</color> <!-- Main background -->
<color name="card_background">#1F2937</color> <!-- Card/Container background -->
<color name="text_primary">#FFFFFF</color>
<color name="text_secondary">#9CA3AF</color>
<color name="border_color">#374151</color>
<color name="background_purple">#0F172A</color>
<!-- Duolingo-inspired Palette - adjustments for dark mode if needed -->
<color name="primary_purple">#7C3AED</color>
<color name="primary_purple_dark">#6D28D9</color>
</resources>

View File

@@ -0,0 +1,18 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme for Night mode. -->
<style name="Theme.Fluxup" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/primary_purple</item>
<item name="colorPrimaryVariant">@color/primary_purple_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/reward_yellow</item>
<item name="colorSecondaryVariant">@color/reward_yellow</item>
<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>
<!-- Customize your theme here. -->
<item name="android:windowBackground">@color/background_light</item>
</style>
</resources>