From a5ee481b391e47170f49ab948125ecd1494070ff Mon Sep 17 00:00:00 2001 From: 230412 <230412@epvc.pt> Date: Tue, 10 Mar 2026 15:34:35 +0000 Subject: [PATCH] first commit --- .gitignore | 15 + .idea/.gitignore | 3 + .idea/.name | 1 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/gradle.xml | 19 + .idea/migrations.xml | 10 + .idea/misc.xml | 10 + .idea/runConfigurations.xml | 17 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle.kts | 46 ++ app/proguard-rules.pro | 21 + .../example/pap/ExampleInstrumentedTest.java | 26 ++ app/src/main/AndroidManifest.xml | 54 +++ .../java/com/example/pap/ChatActivity.java | 138 ++++++ .../com/example/pap/DesafiosActivity.java | 147 ++++++ .../com/example/pap/EstatisticasActivity.java | 26 ++ .../java/com/example/pap/FotoActivity.java | 146 ++++++ .../java/com/example/pap/HomeActivity.java | 135 ++++++ .../java/com/example/pap/Ingrediente.java | 25 ++ .../com/example/pap/IngredientesAdapter.java | 79 ++++ .../java/com/example/pap/LoginActivity.java | 93 ++++ .../java/com/example/pap/MainActivity.java | 24 + .../java/com/example/pap/PerfilActivity.java | 39 ++ .../com/example/pap/RegisterActivity.java | 134 ++++++ .../java/com/example/pap/SupabaseApi.java | 65 +++ .../java/com/example/pap/SupabaseConfig.java | 10 + .../res/drawable/ic_launcher_background.xml | 170 +++++++ .../res/drawable/ic_launcher_foreground.xml | 30 ++ app/src/main/res/layout/activity_chat.xml | 83 ++++ app/src/main/res/layout/activity_desafios.xml | 149 +++++++ .../main/res/layout/activity_estatisticas.xml | 82 ++++ app/src/main/res/layout/activity_foto.xml | 129 ++++++ app/src/main/res/layout/activity_home.xml | 418 ++++++++++++++++++ .../main/res/layout/activity_ingrediente.xml | 39 ++ app/src/main/res/layout/activity_login.xml | 88 ++++ app/src/main/res/layout/activity_main.xml | 19 + app/src/main/res/layout/activity_perfil.xml | 80 ++++ app/src/main/res/layout/activity_register.xml | 152 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 7 + app/src/main/res/values/colors.xml | 5 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 9 + app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 + .../java/com/example/pap/ExampleUnitTest.java | 17 + build.gradle.kts | 4 + gradle.properties | 21 + gradle/libs.versions.toml | 22 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++ gradlew.bat | 89 ++++ settings.gradle.kts | 24 + 68 files changed, 3187 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/example/pap/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/example/pap/ChatActivity.java create mode 100644 app/src/main/java/com/example/pap/DesafiosActivity.java create mode 100644 app/src/main/java/com/example/pap/EstatisticasActivity.java create mode 100644 app/src/main/java/com/example/pap/FotoActivity.java create mode 100644 app/src/main/java/com/example/pap/HomeActivity.java create mode 100644 app/src/main/java/com/example/pap/Ingrediente.java create mode 100644 app/src/main/java/com/example/pap/IngredientesAdapter.java create mode 100644 app/src/main/java/com/example/pap/LoginActivity.java create mode 100644 app/src/main/java/com/example/pap/MainActivity.java create mode 100644 app/src/main/java/com/example/pap/PerfilActivity.java create mode 100644 app/src/main/java/com/example/pap/RegisterActivity.java create mode 100644 app/src/main/java/com/example/pap/SupabaseApi.java create mode 100644 app/src/main/java/com/example/pap/SupabaseConfig.java create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/layout/activity_chat.xml create mode 100644 app/src/main/res/layout/activity_desafios.xml create mode 100644 app/src/main/res/layout/activity_estatisticas.xml create mode 100644 app/src/main/res/layout/activity_foto.xml create mode 100644 app/src/main/res/layout/activity_home.xml create mode 100644 app/src/main/res/layout/activity_ingrediente.xml create mode 100644 app/src/main/res/layout/activity_login.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_perfil.xml create mode 100644 app/src/main/res/layout/activity_register.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/test/java/com/example/pap/ExampleUnitTest.java create mode 100644 build.gradle.kts create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..9cf91de --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +PAP \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..74dd639 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..68b0ad3 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.pap" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.pap" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/pap/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/pap/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f703a1d --- /dev/null +++ b/app/src/androidTest/java/com/example/pap/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.pap; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.pap", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6f76c78 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/ChatActivity.java b/app/src/main/java/com/example/pap/ChatActivity.java new file mode 100644 index 0000000..3df565b --- /dev/null +++ b/app/src/main/java/com/example/pap/ChatActivity.java @@ -0,0 +1,138 @@ +package com.example.pap; + +import android.graphics.Color; +import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; + +public class ChatActivity extends AppCompatActivity { + + private LinearLayout chatLayout; + private ScrollView chatScrollView; + private EditText etNovaMensagem; + private Button btnEnviarMensagem; + private Button btnVoltarChat; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + + // 1. Associar os componentes do layout + chatLayout = findViewById(R.id.chatLayout); + chatScrollView = findViewById(R.id.chatScrollView); + etNovaMensagem = findViewById(R.id.etNovaMensagem); + btnEnviarMensagem = findViewById(R.id.btnEnviarMensagem); + btnVoltarChat = findViewById(R.id.btnVoltarChat); + + // Mensagem inicial da IA ao abrir o ecrã + adicionarMensagemBalao("Olá! Sou o teu IA Coach de Saúde. Como te posso ajudar hoje na tua jornada de emagrecimento?", false); + + // 2. Clique no botão de Enviar + btnEnviarMensagem.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + String mensagemUser = etNovaMensagem.getText().toString().trim(); + + if (!mensagemUser.isEmpty()) { + // Adiciona a mensagem do utilizador no ecrã + adicionarMensagemBalao(mensagemUser, true); + + // Limpa a caixa de texto + etNovaMensagem.setText(""); + + // Simula a IA a processar e a responder + simularRespostaDaIA(mensagemUser); + } + } + }); + + // 3. Clique no botão de Voltar + btnVoltarChat.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } + + // Passo 3: Função para desenhar os balões de conversa no ecrã + private void adicionarMensagemBalao(String texto, boolean isUser) { + TextView textView = new TextView(this); + textView.setText(texto); + textView.setTextSize(16f); + textView.setPadding(32, 24, 32, 24); + + // Configurar as margens e o alinhamento (Direita = Utilizador, Esquerda = IA) + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + params.setMargins(0, 16, 0, 16); + + // Configurar o design arredondado do balão + GradientDrawable background = new GradientDrawable(); + background.setCornerRadius(32f); + + if (isUser) { + // Estilo do Utilizador (Fundo Verde, Texto Branco, Alinhado à direita) + params.gravity = Gravity.END; + background.setColor(Color.parseColor("#4CAF50")); + textView.setTextColor(Color.WHITE); + } else { + // Estilo da IA (Fundo Cinza/Branco, Texto Escuro, Alinhado à esquerda) + params.gravity = Gravity.START; + background.setColor(Color.parseColor("#FFFFFF")); + textView.setTextColor(Color.parseColor("#2E3D32")); + textView.setElevation(4f); // Dá uma pequena sombra ao balão da IA + } + + textView.setLayoutParams(params); + textView.setBackground(background); + + // Adicionar o balão ao ecrã + chatLayout.addView(textView); + + // Fazer scroll automático para baixo para ver a nova mensagem + chatScrollView.post(new Runnable() { + @Override + public void run() { + chatScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + } + + // Passo 4: Função para simular a inteligência da IA + private void simularRespostaDaIA(String perguntaUtilizador) { + perguntaUtilizador = perguntaUtilizador.toLowerCase(); + final String resposta; + + // Regras simples para fingir que a IA está a entender a pergunta + if (perguntaUtilizador.contains("água") || perguntaUtilizador.contains("agua")) { + resposta = "A hidratação é fundamental! O ideal é beberes pelo menos 2 a 3 litros de água por dia. Já bebeste algum copo hoje?"; + } else if (perguntaUtilizador.contains("comer") || perguntaUtilizador.contains("fome") || perguntaUtilizador.contains("dieta")) { + resposta = "Se sentires fome entre as refeições, opta por snacks saudáveis como uma peça de fruta ou um punhado de frutos secos. Evita os doces!"; + } else if (perguntaUtilizador.contains("treino") || perguntaUtilizador.contains("exercício") || perguntaUtilizador.contains("correr")) { + resposta = "Excelente! O exercício acelera o metabolismo. Lembra-te que a consistência é mais importante do que a intensidade. 30 minutos de caminhada já fazem a diferença."; + } else { + resposta = "Essa é uma ótima questão! O segredo do emagrecimento é o défice calórico aliado a bons hábitos. Continua focado, estou aqui para te apoiar em cada passo!"; + } + + // Simular um tempo de "pensamento" da IA (espera 1.5 segundos antes de mostrar a resposta) + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + adicionarMensagemBalao(resposta, false); + } + }, 1500); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/DesafiosActivity.java b/app/src/main/java/com/example/pap/DesafiosActivity.java new file mode 100644 index 0000000..7080206 --- /dev/null +++ b/app/src/main/java/com/example/pap/DesafiosActivity.java @@ -0,0 +1,147 @@ +package com.example.pap; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; + +public class DesafiosActivity extends AppCompatActivity { + + private TextView tvPontos, tvStatusDesafio1, tvStatusDesafio2; + private Button btnVideoDesafio1, btnVideoDesafio2, btnVoltarDesafios; + + private int pontosAtuais = 0; + private SharedPreferences sharedPreferences; + + // Variável para saber qual desafio está a ser gravado no momento + private int desafioAtualEmGravacao = -1; + + // Ferramenta para abrir a câmara de vídeo e receber o resultado + private final ActivityResultLauncher videoLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK) { + // O utilizador gravou o vídeo com sucesso! + Toast.makeText(this, "Vídeo capturado! A enviar para a IA...", Toast.LENGTH_SHORT).show(); + simularAnaliseDaIA(desafioAtualEmGravacao); + } else { + Toast.makeText(this, "Gravação cancelada.", Toast.LENGTH_SHORT).show(); + } + } + ); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_desafios); + + // 1. Inicializar os componentes + tvPontos = findViewById(R.id.tvPontos); + tvStatusDesafio1 = findViewById(R.id.tvStatusDesafio1); + tvStatusDesafio2 = findViewById(R.id.tvStatusDesafio2); + btnVideoDesafio1 = findViewById(R.id.btnVideoDesafio1); + btnVideoDesafio2 = findViewById(R.id.btnVideoDesafio2); + btnVoltarDesafios = findViewById(R.id.btnVoltarDesafios); + + // 2. Carregar os dados guardados no telemóvel + sharedPreferences = getSharedPreferences("AppEmagrecimento", MODE_PRIVATE); + carregarDadosGuardados(); + + // 3. Configurar os cliques para gravar vídeo + btnVideoDesafio1.setOnClickListener(v -> abrirCameraVideo(1)); + btnVideoDesafio2.setOnClickListener(v -> abrirCameraVideo(2)); + + // 4. Botão de voltar + btnVoltarDesafios.setOnClickListener(v -> finish()); + } + + // Função para abrir a câmara do telemóvel em modo VÍDEO + private void abrirCameraVideo(int numeroDoDesafio) { + desafioAtualEmGravacao = numeroDoDesafio; + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + + try { + videoLauncher.launch(intent); + } catch (ActivityNotFoundException e) { + // Caso o emulador não tenha uma câmara configurada, abre a galeria para selecionar um vídeo + Toast.makeText(this, "Câmara não encontrada. Selecione um vídeo da galeria.", Toast.LENGTH_LONG).show(); + Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT); + galleryIntent.setType("video/*"); + videoLauncher.launch(galleryIntent); + } + } + + // Função que simula o tempo de pensamento da Inteligência Artificial + private void simularAnaliseDaIA(int numeroDoDesafio) { + TextView tvStatusAtual = (numeroDoDesafio == 1) ? tvStatusDesafio1 : tvStatusDesafio2; + Button btnAtual = (numeroDoDesafio == 1) ? btnVideoDesafio1 : btnVideoDesafio2; + + // Muda a interface para mostrar que a IA está a trabalhar + tvStatusAtual.setText("A IA está a analisar os teus movimentos..."); + tvStatusAtual.setTextColor(Color.parseColor("#2196F3")); // Azul + btnAtual.setEnabled(false); + btnAtual.setText("A processar..."); + + // Simula uma espera de 3.5 segundos (tempo que a IA demoraria a analisar o vídeo) + new Handler(Looper.getMainLooper()).postDelayed(() -> { + + // Depois de 3.5 segundos, a IA aprova o desafio! + int pontosGanhos = (numeroDoDesafio == 1) ? 20 : 30; + + // Atualizar UI + tvStatusAtual.setText("Aprovado pela IA! +" + pontosGanhos + " pts"); + tvStatusAtual.setTextColor(Color.parseColor("#4CAF50")); // Verde + btnAtual.setText("Desafio Concluído"); + btnAtual.setBackgroundTintList(android.content.res.ColorStateList.valueOf(Color.parseColor("#81C784"))); + + // Adicionar pontos e guardar + adicionarPontosEGuardar(numeroDoDesafio, pontosGanhos); + + }, 3500); // 3500 milissegundos = 3.5 segundos + } + + private void adicionarPontosEGuardar(int numeroDoDesafio, int pontos) { + pontosAtuais += pontos; + tvPontos.setText(String.valueOf(pontosAtuais)); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt("pontosTotais", pontosAtuais); + editor.putBoolean("desafio_" + numeroDoDesafio + "_concluido", true); + editor.apply(); + } + + private void carregarDadosGuardados() { + pontosAtuais = sharedPreferences.getInt("pontosTotais", 0); + tvPontos.setText(String.valueOf(pontosAtuais)); + + // Verificar se o Desafio 1 já foi feito anteriormente + if (sharedPreferences.getBoolean("desafio_1_concluido", false)) { + tvStatusDesafio1.setText("Aprovado pela IA!"); + tvStatusDesafio1.setTextColor(Color.parseColor("#4CAF50")); + btnVideoDesafio1.setEnabled(false); + btnVideoDesafio1.setText("Desafio Concluído"); + btnVideoDesafio1.setBackgroundTintList(android.content.res.ColorStateList.valueOf(Color.parseColor("#81C784"))); + } + + // Verificar se o Desafio 2 já foi feito anteriormente + if (sharedPreferences.getBoolean("desafio_2_concluido", false)) { + tvStatusDesafio2.setText("Aprovado pela IA!"); + tvStatusDesafio2.setTextColor(Color.parseColor("#4CAF50")); + btnVideoDesafio2.setEnabled(false); + btnVideoDesafio2.setText("Desafio Concluído"); + btnVideoDesafio2.setBackgroundTintList(android.content.res.ColorStateList.valueOf(Color.parseColor("#81C784"))); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/EstatisticasActivity.java b/app/src/main/java/com/example/pap/EstatisticasActivity.java new file mode 100644 index 0000000..2bd6fdd --- /dev/null +++ b/app/src/main/java/com/example/pap/EstatisticasActivity.java @@ -0,0 +1,26 @@ +package com.example.pap; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import androidx.appcompat.app.AppCompatActivity; + +public class EstatisticasActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_estatisticas); + + Button btnVoltarHome = findViewById(R.id.btnVoltarHome); + + // Ao clicar no botão voltar, a janela de estatísticas fecha + // e o utilizador regressa naturalmente à HomeActivity que estava por baixo + btnVoltarHome.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/FotoActivity.java b/app/src/main/java/com/example/pap/FotoActivity.java new file mode 100644 index 0000000..49c8148 --- /dev/null +++ b/app/src/main/java/com/example/pap/FotoActivity.java @@ -0,0 +1,146 @@ +package com.example.pap; + +import android.content.ActivityNotFoundException; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import java.util.ArrayList; + +public class FotoActivity extends AppCompatActivity { + + private ImageView ivFotoComida; + private Button btnTirarFoto, btnVoltar, btnAddManual, btnConfirmar; + private LinearLayout layoutResultadosIA; + private TextView tvTotalCalorias, tvNomePrato; + private RecyclerView recyclerView; + private IngredientesAdapter adapter; + private ArrayList listaIngredientes; + + // Lançador da Câmara + private final ActivityResultLauncher cameraLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == RESULT_OK && result.getData() != null) { + // Recebe a miniatura da foto + Bundle extras = result.getData().getExtras(); + Bitmap imageBitmap = (Bitmap) extras.get("data"); + ivFotoComida.setImageBitmap(imageBitmap); + + // Inicia a simulação da IA + Toast.makeText(this, "A analisar a imagem com IA...", Toast.LENGTH_SHORT).show(); + simularAnaliseIA(); + } + } + ); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_foto); + + // Inicializar vistas + ivFotoComida = findViewById(R.id.ivFotoComida); + btnTirarFoto = findViewById(R.id.btnTirarFotoComida); + btnVoltar = findViewById(R.id.btnVoltarFoto); + layoutResultadosIA = findViewById(R.id.layoutResultadosIA); + tvTotalCalorias = findViewById(R.id.tvTotalCalorias); + tvNomePrato = findViewById(R.id.tvNomePratoDetectado); + recyclerView = findViewById(R.id.recyclerViewIngredientes); + btnAddManual = findViewById(R.id.btnAddIngredienteManual); + btnConfirmar = findViewById(R.id.btnConfirmarRefeicao); + + // Configurar a RecyclerView + listaIngredientes = new ArrayList<>(); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new IngredientesAdapter(listaIngredientes); + recyclerView.setAdapter(adapter); + + // Configurar o clique no botão de apagar (lixo) na lista + adapter.setOnItemClickListener(new IngredientesAdapter.OnItemClickListener() { + @Override + public void onDeleteClick(int position) { + removerIngrediente(position); + } + }); + + btnTirarFoto.setOnClickListener(v -> abrirCamera()); + btnVoltar.setOnClickListener(v -> finish()); + + // Botão para simular adicionar um ingrediente extra manualmente + btnAddManual.setOnClickListener(v -> { + listaIngredientes.add(new Ingrediente("Azeite Extra (1 c.sopa)", 120)); + adapter.notifyItemInserted(listaIngredientes.size() - 1); + calcularTotal(); + Toast.makeText(this, "Ingrediente adicionado!", Toast.LENGTH_SHORT).show(); + }); + + btnConfirmar.setOnClickListener(v -> { + Toast.makeText(this, "Refeição registada no diário!", Toast.LENGTH_LONG).show(); + finish(); // Fecha o ecrã após confirmar + }); + } + + private void abrirCamera() { + Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + try { + cameraLauncher.launch(takePictureIntent); + } catch (ActivityNotFoundException e) { + Toast.makeText(this, "Erro ao abrir a câmara.", Toast.LENGTH_SHORT).show(); + } + } + + // Simula a resposta da IA após 2 segundos + private void simularAnaliseIA() { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + // 1. Mostra a área de resultados + layoutResultadosIA.setVisibility(View.VISIBLE); + btnTirarFoto.setText("Tirar Outra Foto"); + + // 2. Simula os dados detetados + tvNomePrato.setText("Prato detetado: Salada César com Frango"); + listaIngredientes.clear(); + listaIngredientes.add(new Ingrediente("Peito de Frango Grelhado (150g)", 240)); + listaIngredientes.add(new Ingrediente("Alface Romana (200g)", 30)); + listaIngredientes.add(new Ingrediente("Molho César (2 c.sopa)", 160)); + listaIngredientes.add(new Ingrediente("Croutons (30g)", 120)); + listaIngredientes.add(new Ingrediente("Queijo Parmesão (20g)", 80)); + + // 3. Atualiza a lista e o total + adapter.notifyDataSetChanged(); + calcularTotal(); + + }, 2000); // Espera 2 segundos + } + + // Remove um item da lista e atualiza o total + private void removerIngrediente(int position) { + Ingrediente removido = listaIngredientes.get(position); + listaIngredientes.remove(position); + adapter.notifyItemRemoved(position); + calcularTotal(); + Toast.makeText(this, "Removido: " + removido.getNome(), Toast.LENGTH_SHORT).show(); + } + + // Percorre a lista e soma as calorias + private void calcularTotal() { + int total = 0; + for (Ingrediente ing : listaIngredientes) { + total += ing.getCalorias(); + } + tvTotalCalorias.setText(total + " kcal"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/HomeActivity.java b/app/src/main/java/com/example/pap/HomeActivity.java new file mode 100644 index 0000000..5dafab2 --- /dev/null +++ b/app/src/main/java/com/example/pap/HomeActivity.java @@ -0,0 +1,135 @@ +package com.example.pap; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.InputType; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.cardview.widget.CardView; + +public class HomeActivity extends AppCompatActivity { + + private SharedPreferences sharedPreferences; + + // Agora são 6 cartões! + private CardView cardPerfil, cardEstatisticas, cardTirarFoto, cardChat, cardDesafios, cardDefinicoes; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_home); + + // 1. Iniciar a memória do telemóvel + sharedPreferences = getSharedPreferences("AppEmagrecimento", MODE_PRIVATE); + + // 2. Ligar os cartões do XML ao código + cardPerfil = findViewById(R.id.cardPerfil); + cardEstatisticas = findViewById(R.id.cardEstatisticas); + cardTirarFoto = findViewById(R.id.cardTirarFoto); + cardChat = findViewById(R.id.cardChat); + cardDesafios = findViewById(R.id.cardDesafios); + cardDefinicoes = findViewById(R.id.cardDefinicoes); + + // 3. Configurar os cliques para abrir os outros ecrãs + cardPerfil.setOnClickListener(v -> { + Toast.makeText(this, "A abrir O Meu Perfil...", Toast.LENGTH_SHORT).show(); + // startActivity(new Intent(HomeActivity.this, PerfilActivity.class)); + }); + + cardEstatisticas.setOnClickListener(v -> { + Toast.makeText(this, "A abrir Estatísticas...", Toast.LENGTH_SHORT).show(); + // startActivity(new Intent(HomeActivity.this, EstatisticasActivity.class)); + }); + + cardTirarFoto.setOnClickListener(v -> { + startActivity(new Intent(HomeActivity.this, FotoActivity.class)); + }); + + cardChat.setOnClickListener(v -> { + startActivity(new Intent(HomeActivity.this, ChatActivity.class)); + }); + + cardDesafios.setOnClickListener(v -> { + startActivity(new Intent(HomeActivity.this, DesafiosActivity.class)); + }); + + cardDefinicoes.setOnClickListener(v -> { + Toast.makeText(this, "A abrir Definições...", Toast.LENGTH_SHORT).show(); + // startActivity(new Intent(HomeActivity.this, DefinicoesActivity.class)); + }); + + // 4. Verifica se já passou o tempo para pedir o novo peso + verificarAtualizacaoSemanal(); + } + + // Função que calcula se já passou o tempo (1 semana ou 10 segundos) + private void verificarAtualizacaoSemanal() { + long dataUltimaAtualizacao = sharedPreferences.getLong("data_ultima_atualizacao", 0); + long dataAtual = System.currentTimeMillis(); + + if (dataUltimaAtualizacao == 0) { + sharedPreferences.edit().putLong("data_ultima_atualizacao", dataAtual).apply(); + } else { + // TEMPO CONFIGURADO PARA TESTES: 10 segundos! + // Para a PAP final usa: long tempoNecessario = 7L * 24 * 60 * 60 * 1000; + long tempoNecessario = 10 * 1000; + + if (dataAtual - dataUltimaAtualizacao >= tempoNecessario) { + mostrarPopupAtualizacao(); + } + } + } + + // Função que cria o pop-up para atualizar o peso + private void mostrarPopupAtualizacao() { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Hora da pesagem! ⚖️"); + builder.setMessage("Já passou 1 semana! Atualiza o teu peso e altura para acompanharmos a tua evolução."); + builder.setCancelable(false); + + LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(50, 40, 50, 10); + + final EditText etNovoPeso = new EditText(this); + etNovoPeso.setHint("Novo Peso (ex: 75.5)"); + etNovoPeso.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + layout.addView(etNovoPeso); + + final EditText etNovaAltura = new EditText(this); + etNovaAltura.setHint("Nova Altura (ex: 1.75)"); + etNovaAltura.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + layout.addView(etNovaAltura); + + builder.setView(layout); + builder.setPositiveButton("Atualizar Dados", null); + + AlertDialog dialog = builder.create(); + dialog.show(); + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + String pesoStr = etNovoPeso.getText().toString().trim(); + String alturaStr = etNovaAltura.getText().toString().trim(); + + if (!pesoStr.isEmpty() && !alturaStr.isEmpty()) { + float novoPeso = Float.parseFloat(pesoStr); + float novaAltura = Float.parseFloat(alturaStr); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putFloat("pesoAtual", novoPeso); + editor.putFloat("alturaAtual", novaAltura); + editor.putLong("data_ultima_atualizacao", System.currentTimeMillis()); + editor.apply(); + + Toast.makeText(HomeActivity.this, "Dados atualizados com sucesso! 💪", Toast.LENGTH_SHORT).show(); + dialog.dismiss(); + } else { + Toast.makeText(HomeActivity.this, "Tens de preencher os dois campos!", Toast.LENGTH_SHORT).show(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/Ingrediente.java b/app/src/main/java/com/example/pap/Ingrediente.java new file mode 100644 index 0000000..3354670 --- /dev/null +++ b/app/src/main/java/com/example/pap/Ingrediente.java @@ -0,0 +1,25 @@ +package com.example.pap; + +// Esta classe é um modelo simples para guardar os dados de um ingrediente +public class Ingrediente { + private String nome; + private int calorias; + + public Ingrediente(String nome, int calorias) { + this.nome = nome; + this.calorias = calorias; + } + + public String getNome() { + return nome; + } + + public int getCalorias() { + return calorias; + } + + // Vamos precisar disto para alterar a quantidade se quisermos no futuro + public void setCalorias(int calorias) { + this.calorias = calorias; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/IngredientesAdapter.java b/app/src/main/java/com/example/pap/IngredientesAdapter.java new file mode 100644 index 0000000..a7d0fdf --- /dev/null +++ b/app/src/main/java/com/example/pap/IngredientesAdapter.java @@ -0,0 +1,79 @@ +package com.example.pap; + +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.recyclerview.widget.RecyclerView; +import java.util.ArrayList; + +// O Adapter é o motor que liga os dados à lista visual +public class IngredientesAdapter extends RecyclerView.Adapter { + + private ArrayList listaIngredientes; + private OnItemClickListener listener; + + // Interface para avisar a Activity que um item foi apagado + public interface OnItemClickListener { + void onDeleteClick(int position); + } + + public void setOnItemClickListener(OnItemClickListener listener) { + this.listener = listener; + } + + // Construtor + public IngredientesAdapter(ArrayList listaIngredientes) { + this.listaIngredientes = listaIngredientes; + } + + // Cria a "caixinha" visual para cada linha + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.activity_ingrediente, parent, false); + return new ViewHolder(view, listener); + } + + // Preenche a "caixinha" com os dados do ingrediente atual + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + Ingrediente ingredienteAtual = listaIngredientes.get(position); + holder.tvNome.setText(ingredienteAtual.getNome()); + holder.tvCalorias.setText(ingredienteAtual.getCalorias() + " kcal"); + } + + @Override + public int getItemCount() { + return listaIngredientes.size(); + } + + // Classe interna que guarda as referências aos elementos do layout de cada linha + public static class ViewHolder extends RecyclerView.ViewHolder { + public TextView tvNome; + public TextView tvCalorias; + public ImageButton btnDelete; + + public ViewHolder(@NonNull View itemView, final OnItemClickListener listener) { + super(itemView); + tvNome = itemView.findViewById(R.id.tvNomeIngrediente); + tvCalorias = itemView.findViewById(R.id.tvCaloriasIngrediente); + btnDelete = itemView.findViewById(R.id.btnRemoverIngrediente); + + // Configura o clique no botão de lixo + btnDelete.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + int position = getAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onDeleteClick(position); + } + } + } + }); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/LoginActivity.java b/app/src/main/java/com/example/pap/LoginActivity.java new file mode 100644 index 0000000..aa1441e --- /dev/null +++ b/app/src/main/java/com/example/pap/LoginActivity.java @@ -0,0 +1,93 @@ +package com.example.pap; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class LoginActivity extends AppCompatActivity { + + private EditText etLoginEmail, etLoginPassword; + private Button btnLogin; + private TextView tvGoToRegister; + private SupabaseApi api; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + // 1. Inicializar os componentes + etLoginEmail = findViewById(R.id.etLoginEmail); + etLoginPassword = findViewById(R.id.etLoginPassword); + btnLogin = findViewById(R.id.btnLogin); + tvGoToRegister = findViewById(R.id.tvGoToRegister); + + // 2. Iniciar a comunicação com a API do Supabase + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(SupabaseConfig.URL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + api = retrofit.create(SupabaseApi.class); + + // 3. Configurar os cliques + btnLogin.setOnClickListener(v -> efetuarLogin()); + + tvGoToRegister.setOnClickListener(v -> { + Intent intent = new Intent(LoginActivity.this, RegisterActivity.class); + startActivity(intent); + }); + } + + private void efetuarLogin() { + String email = etLoginEmail.getText().toString().trim(); + String password = etLoginPassword.getText().toString().trim(); + + // Validação básica + if (email.isEmpty() || password.isEmpty()) { + Toast.makeText(this, "Preencha o email e a password!", Toast.LENGTH_SHORT).show(); + return; + } + + UserCredentials credentials = new UserCredentials(email, password); + + // Faz o pedido de Login ao Supabase + api.login(SupabaseConfig.API_KEY, credentials).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + + // Login com sucesso! Vai para a Home + Toast.makeText(LoginActivity.this, "Login com sucesso!", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(LoginActivity.this, HomeActivity.class); + startActivity(intent); + finish(); // Fecha a tela de login + + } else { + // SE DER ERRO (ex: 404, 400), MOSTRA O LINK EXATO E O CÓDIGO + String urlQueFalhou = response.raw().request().url().toString(); + if (response.code() == 400) { + Toast.makeText(LoginActivity.this, "Email ou password incorretos!", Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(LoginActivity.this, "Erro " + response.code() + "! Link: " + urlQueFalhou, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(LoginActivity.this, "Falha na ligação: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/MainActivity.java b/app/src/main/java/com/example/pap/MainActivity.java new file mode 100644 index 0000000..87851ba --- /dev/null +++ b/app/src/main/java/com/example/pap/MainActivity.java @@ -0,0 +1,24 @@ +package com.example.pap; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/PerfilActivity.java b/app/src/main/java/com/example/pap/PerfilActivity.java new file mode 100644 index 0000000..83651e3 --- /dev/null +++ b/app/src/main/java/com/example/pap/PerfilActivity.java @@ -0,0 +1,39 @@ +package com.example.pap; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import androidx.appcompat.app.AppCompatActivity; + +public class PerfilActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_perfil); + + Button btnVoltar = findViewById(R.id.btnVoltarPerfil); + Button btnSair = findViewById(R.id.btnSair); + + // Voltar à Home + btnVoltar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + + // Terminar Sessão (Voltar ao Login e limpar a Home) + btnSair.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(PerfilActivity.this, LoginActivity.class); + // Estas flags garantem que o utilizador não consegue clicar em "Voltar" no telemóvel e ir para a Home de novo + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/RegisterActivity.java b/app/src/main/java/com/example/pap/RegisterActivity.java new file mode 100644 index 0000000..ce9e85e --- /dev/null +++ b/app/src/main/java/com/example/pap/RegisterActivity.java @@ -0,0 +1,134 @@ +package com.example.pap; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class RegisterActivity extends AppCompatActivity { + + private EditText etRegNome, etRegEmail, etRegPassword, etRegAltura, etRegPeso; + private Button btnRegister; + private TextView tvGoToLogin; + private SupabaseApi api; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_register); + + // 1. Ligar os elementos do design ao código + etRegNome = findViewById(R.id.etRegNome); + etRegEmail = findViewById(R.id.etRegEmail); + etRegPassword = findViewById(R.id.etRegPassword); + etRegAltura = findViewById(R.id.etRegAltura); + etRegPeso = findViewById(R.id.etRegPeso); + btnRegister = findViewById(R.id.btnRegister); + tvGoToLogin = findViewById(R.id.tvGoToLogin); + + // 2. Configurar o Retrofit (A ponte de comunicação com o Supabase) + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(SupabaseConfig.URL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + api = retrofit.create(SupabaseApi.class); + + // 3. Configurar os cliques dos botões + btnRegister.setOnClickListener(v -> efetuarRegisto()); + tvGoToLogin.setOnClickListener(v -> finish()); + } + + private void efetuarRegisto() { + String nome = etRegNome.getText().toString().trim(); + String email = etRegEmail.getText().toString().trim(); + String password = etRegPassword.getText().toString().trim(); + String pesoStr = etRegPeso.getText().toString().trim(); + String alturaStr = etRegAltura.getText().toString().trim(); + + // Verificações para garantir que o utilizador preencheu tudo + if (nome.isEmpty() || email.isEmpty() || password.isEmpty() || pesoStr.isEmpty() || alturaStr.isEmpty()) { + Toast.makeText(this, "Preencha todos os campos!", Toast.LENGTH_SHORT).show(); + return; + } + + if (password.length() < 6) { + Toast.makeText(this, "A password precisa de pelo menos 6 caracteres.", Toast.LENGTH_SHORT).show(); + return; + } + + float peso = Float.parseFloat(pesoStr); + float altura = Float.parseFloat(alturaStr); + + // FASE 1: Criar a conta (Email e Password) no Supabase Auth + UserCredentials credentials = new UserCredentials(email, password); + + api.signUp(SupabaseConfig.API_KEY, credentials).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + + // Sucesso! A conta foi criada. Vamos extrair o ID e o Token + String userId = response.body().id != null ? response.body().id : response.body().user.id; + String token = "Bearer " + response.body().access_token; + + // FASE 2: Guardar o Nome, Peso e Altura na tabela "profiles" + salvarPerfil(userId, nome, email, peso, altura, token); + + } else { + String urlQueFalhou = response.raw().request().url().toString(); + if (response.code() == 400) { + Toast.makeText(RegisterActivity.this, "Erro 400: Este email já existe ou a password é fraca.", Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(RegisterActivity.this, "Erro " + response.code() + " no Auth! Link: " + urlQueFalhou, Toast.LENGTH_LONG).show(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(RegisterActivity.this, "Falha de rede (Sem internet?): " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + // Método responsável por guardar os dados extras na tabela profiles + private void salvarPerfil(String id, String nome, String email, float peso, float altura, String token) { + ProfileData profile = new ProfileData(id, nome, email, peso, altura); + + api.insertProfile(SupabaseConfig.API_KEY, token, "application/json", "return=minimal", profile) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + // TUDO CORREU BEM! + Toast.makeText(RegisterActivity.this, "Conta e Perfil criados com sucesso!", Toast.LENGTH_LONG).show(); + finish(); // Volta para o ecrã de login + } else { + // DEU ERRO A GUARDAR O PESO E ALTURA. VAMOS LER O ERRO EXATO! + try { + String erroExato = response.errorBody().string(); + // Esta mensagem vai mostrar-te o que está mal configurado no painel do Supabase + Toast.makeText(RegisterActivity.this, "ERRO DA BASE DE DADOS: " + erroExato, Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Toast.makeText(RegisterActivity.this, "Erro a ler a resposta da base de dados.", Toast.LENGTH_SHORT).show(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(RegisterActivity.this, "Erro ao conectar à base de dados.", Toast.LENGTH_SHORT).show(); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/SupabaseApi.java b/app/src/main/java/com/example/pap/SupabaseApi.java new file mode 100644 index 0000000..f832fd1 --- /dev/null +++ b/app/src/main/java/com/example/pap/SupabaseApi.java @@ -0,0 +1,65 @@ +package com.example.pap; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Header; +import retrofit2.http.POST; + +public interface SupabaseApi { + + // Rota para criar a conta + @POST("auth/v1/signup") + Call signUp(@Header("apikey") String apiKey, @Body UserCredentials credentials); + + // Rota para fazer login + @POST("auth/v1/token?grant_type=password") + Call login(@Header("apikey") String apiKey, @Body UserCredentials credentials); + + // Rota para guardar o Nome, Peso e Altura na base de dados + @POST("rest/v1/profiles") + Call insertProfile( + @Header("apikey") String apiKey, + @Header("Authorization") String token, + @Header("Content-Type") String contentType, + @Header("Prefer") String prefer, + @Body ProfileData profile + ); +} + +// ---- Classes Auxiliares para Formatarem os Dados ---- + +class UserCredentials { + String email; + String password; + + public UserCredentials(String email, String password) { + this.email = email; + this.password = password; + } +} + +class ProfileData { + String id; + String nome; + String email; + float peso; + float altura; + + public ProfileData(String id, String nome, String email, float peso, float altura) { + this.id = id; + this.nome = nome; + this.email = email; + this.peso = peso; + this.altura = altura; + } +} + +class SupabaseResponse { + String id; + String access_token; + UserResponse user; + + class UserResponse { + String id; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/pap/SupabaseConfig.java b/app/src/main/java/com/example/pap/SupabaseConfig.java new file mode 100644 index 0000000..5393356 --- /dev/null +++ b/app/src/main/java/com/example/pap/SupabaseConfig.java @@ -0,0 +1,10 @@ +package com.example.pap; + +public class SupabaseConfig { + + // O URL limpo, apenas com a barra no final! + public static final String URL = "https://lkjbbbgavoyknuxaskho.supabase.co"; + + // A tua chave está perfeita + public static final String API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxramJiYmdhdm95a251eGFza2hvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI2MjM4OTMsImV4cCI6MjA4ODE5OTg5M30.HV9ZXYCaF1V8dZwPxv_p5_gDi9cN_ioumDm9mgmEQPU"; +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_chat.xml b/app/src/main/res/layout/activity_chat.xml new file mode 100644 index 0000000..29894ac --- /dev/null +++ b/app/src/main/res/layout/activity_chat.xml @@ -0,0 +1,83 @@ + + + + + +