Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6db58f299 | ||
|
|
25d6ef6cdb |
@@ -41,6 +41,7 @@ Saludapp es una aplicación Android moderna enfocada en la salud y el bienestar,
|
||||
- `StartScreen.kt`: Pantalla de bienvenida.
|
||||
- `LoginScreen.kt`: Inicio de sesión con validación de credenciales encriptadas.
|
||||
- `RegisterScreen.kt`: Registro de nuevos usuarios con soporte para nombre completo.
|
||||
- `HomeScreen.kt`: Dashboard principal con resumen de salud (pasos, sueño, agua, etc.).
|
||||
- `theme/`: Configuración estética de Material 3.
|
||||
|
||||
---
|
||||
|
||||
@@ -42,6 +42,7 @@ dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
|
||||
@@ -4,48 +4,55 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.example.saludapp.ui.HomeScreen
|
||||
import com.example.saludapp.ui.LoginScreen
|
||||
import com.example.saludapp.ui.RegisterScreen
|
||||
import com.example.saludapp.ui.StartScreen
|
||||
import com.example.saludapp.ui.theme.SaludappTheme
|
||||
|
||||
/**
|
||||
* Actividad principal de la aplicación.
|
||||
* Gestiona el flujo de navegación entre pantallas y el estado de la sesión.
|
||||
*/
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Habilitar diseño de borde a borde (edge-to-edge)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
SaludappTheme {
|
||||
// Estado para controlar en qué pantalla se encuentra el usuario
|
||||
var currentScreen by remember { mutableStateOf("start") }
|
||||
// Almacena el nombre del usuario logueado para mostrarlo en el Dashboard
|
||||
var loggedInUserName by remember { mutableStateOf("") }
|
||||
|
||||
// Enrutador simple basado en el estado 'currentScreen'
|
||||
when (currentScreen) {
|
||||
"start" -> StartScreen {
|
||||
currentScreen = "login"
|
||||
}
|
||||
"login" -> LoginScreen(
|
||||
onLoginSuccess = { currentScreen = "home" },
|
||||
onLoginSuccess = { name ->
|
||||
loggedInUserName = name
|
||||
currentScreen = "home"
|
||||
},
|
||||
onNavigateToRegister = { currentScreen = "register" }
|
||||
)
|
||||
"register" -> RegisterScreen(
|
||||
onRegisterSuccess = { currentScreen = "login" },
|
||||
onBackToLogin = { currentScreen = "login" }
|
||||
)
|
||||
"home" -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Bienvenido a Saludapp")
|
||||
"home" -> HomeScreen(
|
||||
userName = loggedInUserName,
|
||||
onLogout = {
|
||||
loggedInUserName = ""
|
||||
currentScreen = "start"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,20 @@ import java.sql.Connection
|
||||
import java.sql.DriverManager
|
||||
import java.sql.SQLException
|
||||
|
||||
/**
|
||||
* Objeto encargado de gestionar todas las operaciones con la base de datos MariaDB.
|
||||
* Implementa seguridad mediante hashing de contraseñas y operaciones asíncronas.
|
||||
*/
|
||||
object DatabaseHelper {
|
||||
// Configuración de la conexión al servidor externo
|
||||
private const val URL = "jdbc:mariadb://mariadb.peta9.com:3306/saludapp_db"
|
||||
private const val USER = "saludapp_dba"
|
||||
private const val PASSWORD = "Pedro@110387"
|
||||
|
||||
/**
|
||||
* Encripta la contraseña utilizando el algoritmo SHA-256 para mayor seguridad.
|
||||
* Retorna la representación hexadecimal del hash.
|
||||
*/
|
||||
private fun hashPassword(password: String): String {
|
||||
val bytes = password.toByteArray()
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
@@ -19,14 +28,22 @@ object DatabaseHelper {
|
||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
suspend fun validateLogin(user: String, pass: String): Boolean = withContext(Dispatchers.IO) {
|
||||
/**
|
||||
* Valida las credenciales de un usuario.
|
||||
* @param user Nombre de usuario o correo electrónico.
|
||||
* @param pass Contraseña en texto plano (será hasheada antes de comparar).
|
||||
* @return El nombre completo del usuario si tiene éxito, null de lo contrario.
|
||||
*/
|
||||
suspend fun validateLogin(user: String, pass: String): String? = withContext(Dispatchers.IO) {
|
||||
var connection: Connection? = null
|
||||
try {
|
||||
// Cargar el driver de MariaDB y establecer conexión
|
||||
Class.forName("org.mariadb.jdbc.Driver")
|
||||
connection = DriverManager.getConnection(URL, USER, PASSWORD)
|
||||
|
||||
val hashedPass = hashPassword(pass)
|
||||
val query = "SELECT COUNT(*) FROM usuarios WHERE (username = ? OR email = ?) AND password = ?"
|
||||
// Consulta preparada para evitar ataques de inyección SQL
|
||||
val query = "SELECT nombre_completo FROM usuarios WHERE (username = ? OR email = ?) AND password = ?"
|
||||
connection.prepareStatement(query).use { statement ->
|
||||
statement.setString(1, user)
|
||||
statement.setString(2, user)
|
||||
@@ -34,21 +51,31 @@ object DatabaseHelper {
|
||||
|
||||
val resultSet = statement.executeQuery()
|
||||
if (resultSet.next()) {
|
||||
return@withContext resultSet.getInt(1) > 0
|
||||
// Retorna el nombre real del usuario guardado en la DB
|
||||
return@withContext resultSet.getString("nombre_completo") ?: user
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
// Asegurar que la conexión se cierra siempre
|
||||
try {
|
||||
connection?.close()
|
||||
} catch (e: SQLException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
return@withContext false
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra un nuevo usuario en la base de datos.
|
||||
* @param user Nombre de usuario único.
|
||||
* @param email Correo electrónico único.
|
||||
* @param pass Contraseña en texto plano (se guardará como hash SHA-256).
|
||||
* @param fullName Nombre completo del usuario.
|
||||
* @return true si el registro fue exitoso, false en caso contrario.
|
||||
*/
|
||||
suspend fun registerUser(user: String, email: String, pass: String, fullName: String): Boolean = withContext(Dispatchers.IO) {
|
||||
var connection: Connection? = null
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.example.saludapp.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.DirectionsRun
|
||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
* Pantalla principal (Dashboard) que se muestra tras un inicio de sesión exitoso.
|
||||
* @param userName Nombre del usuario para el mensaje de bienvenida.
|
||||
* @param onLogout Acción a ejecutar cuando el usuario decide cerrar sesión.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(userName: String, onLogout: () -> Unit) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Saludapp", fontWeight = FontWeight.Bold) },
|
||||
actions = {
|
||||
// Botón para cerrar la sesión actual
|
||||
IconButton(onClick = onLogout) {
|
||||
Icon(Icons.AutoMirrored.Filled.ExitToApp, contentDescription = "Cerrar sesión")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Mensaje de bienvenida personalizado
|
||||
Text(
|
||||
text = "Hola, $userName 👋",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "Aquí tienes tu resumen de hoy",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Cuadrícula de tarjetas con estadísticas de salud
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
StatCard("Pasos", "8,432", Icons.AutoMirrored.Filled.DirectionsRun, MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
item {
|
||||
StatCard("Sueño", "7h 20m", Icons.Default.Bedtime, MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
item {
|
||||
StatCard("Agua", "1.5L", Icons.Default.LocalDrink, MaterialTheme.colorScheme.tertiary)
|
||||
}
|
||||
item {
|
||||
StatCard("Calorías", "1,200 kcal", Icons.Default.Whatshot, MaterialTheme.colorScheme.error)
|
||||
}
|
||||
item {
|
||||
StatCard("Corazón", "72 bpm", Icons.Default.Favorite, MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
item {
|
||||
StatCard("Peso", "70 kg", Icons.Default.MonitorWeight, MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Componente reutilizable para mostrar una tarjeta de estadística.
|
||||
* @param title Título del dato (ej. "Agua").
|
||||
* @param value Valor actual (ej. "1.5L").
|
||||
* @param icon Icono representativo.
|
||||
* @param color Color temático para la tarjeta y el icono.
|
||||
*/
|
||||
@Composable
|
||||
fun StatCard(title: String, value: String, icon: ImageVector, color: androidx.compose.ui.graphics.Color) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(140.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = color,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = value,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import com.example.saludapp.data.DatabaseHelper
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(onLoginSuccess: () -> Unit, onNavigateToRegister: () -> Unit) {
|
||||
fun LoginScreen(onLoginSuccess: (String) -> Unit, onNavigateToRegister: () -> Unit) {
|
||||
var email by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
@@ -73,10 +73,10 @@ fun LoginScreen(onLoginSuccess: () -> Unit, onNavigateToRegister: () -> Unit) {
|
||||
scope.launch {
|
||||
isLoading = true
|
||||
errorMessage = null
|
||||
val success = DatabaseHelper.validateLogin(email, password)
|
||||
val userName = DatabaseHelper.validateLogin(email, password)
|
||||
isLoading = false
|
||||
if (success) {
|
||||
onLoginSuccess()
|
||||
if (userName != null) {
|
||||
onLoginSuccess(userName)
|
||||
} else {
|
||||
errorMessage = "Usuario o contraseña incorrectos"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
||||
Reference in New Issue
Block a user