ui ux update, adding calorie counter

This commit is contained in:
HagaDalpintoGinting 2025-12-17 21:06:36 +07:00
parent edbb6a52fb
commit a23005280b
6 changed files with 844 additions and 188 deletions

View File

@ -18,6 +18,7 @@ class PreferencesManager(private val context: Context) {
val USER_NAME = stringPreferencesKey("user_name") val USER_NAME = stringPreferencesKey("user_name")
val STEP_GOAL = intPreferencesKey("step_goal") val STEP_GOAL = intPreferencesKey("step_goal")
val WATER_GOAL = intPreferencesKey("water_goal") val WATER_GOAL = intPreferencesKey("water_goal")
val USER_WEIGHT = intPreferencesKey("user_weight")
} }
val userName: Flow<String> = context.dataStore.data.map { preferences -> val userName: Flow<String> = context.dataStore.data.map { preferences ->
@ -32,6 +33,10 @@ class PreferencesManager(private val context: Context) {
preferences[WATER_GOAL] ?: 2000 preferences[WATER_GOAL] ?: 2000
} }
val userWeight: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[USER_WEIGHT] ?: 70
}
suspend fun saveUserName(name: String) { suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences -> context.dataStore.edit { preferences ->
preferences[USER_NAME] = name preferences[USER_NAME] = name
@ -49,4 +54,9 @@ class PreferencesManager(private val context: Context) {
preferences[WATER_GOAL] = goal preferences[WATER_GOAL] = goal
} }
} }
suspend fun saveUserWeight(weight: Int) {
context.dataStore.edit { preferences ->
preferences[USER_WEIGHT] = weight
}
}
} }

View File

@ -1,23 +1,28 @@
package com.example.stepdrink.ui.screen.home package com.example.stepdrink.ui.screen.home
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DirectionsWalk import androidx.compose.material.icons.filled.DirectionsWalk
import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.WaterDrop import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.example.stepdrink.ui.components.StatCard
import com.example.stepdrink.ui.navigation.Screen import com.example.stepdrink.ui.navigation.Screen
import com.example.stepdrink.viewmodel.StepViewModel import com.example.stepdrink.viewmodel.StepViewModel
import com.example.stepdrink.viewmodel.WaterViewModel import com.example.stepdrink.viewmodel.WaterViewModel
import com.example.stepdrink.viewmodel.ProfileViewModel import com.example.stepdrink.viewmodel.ProfileViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
@ -36,14 +41,44 @@ fun HomeScreen(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { title = {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text( Text(
text = "Step & Drink", text = "Step",
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
) )
Text(
text = "&",
fontWeight = FontWeight.Light,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = "Drink",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.tertiary
)
}
}, },
actions = { actions = {
IconButton(onClick = { navController.navigate(Screen.Profile.route) }) { IconButton(
Icon(Icons.Default.Person, "Profil") onClick = { navController.navigate(Screen.Profile.route) }
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
modifier = Modifier.size(36.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
Icons.Default.Person,
"Profil",
tint = MaterialTheme.colorScheme.primary
)
}
}
} }
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
@ -60,61 +95,241 @@ fun HomeScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Greeting Card
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(
colors = listOf(
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.tertiaryContainer
)
)
)
.padding(20.dp)
) {
Column {
Text( Text(
text = "Halo, $userName! 👋", text = "Halo, $userName! 👋",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text(
text = "Tetap sehat dengan tracking harian",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// Steps Card Text(
StatCard( text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
// Steps Card - Improved
ImprovedStatCard(
icon = Icons.Default.DirectionsWalk, icon = Icons.Default.DirectionsWalk,
title = "Langkah", title = "Langkah",
value = "${todaySteps?.steps ?: 0}", value = "${todaySteps?.steps ?: 0}",
unit = "langkah",
subtitle = "Target: $stepGoal langkah", subtitle = "Target: $stepGoal langkah",
progress = (todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat(), progress = (todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat(),
gradientColors = listOf(
Color(0xFF4CAF50),
Color(0xFF81C784)
),
onClick = { navController.navigate(Screen.Steps.route) } onClick = { navController.navigate(Screen.Steps.route) }
) )
// Water Card // Water Card - Improved
StatCard( ImprovedStatCard(
icon = Icons.Default.WaterDrop, icon = Icons.Default.WaterDrop,
title = "Air Minum", title = "Air Minum",
value = "${todayWater}ml", value = "$todayWater",
unit = "ml",
subtitle = "Target: ${waterGoal}ml", subtitle = "Target: ${waterGoal}ml",
progress = todayWater.toFloat() / waterGoal.toFloat(), progress = todayWater.toFloat() / waterGoal.toFloat(),
gradientColors = listOf(
Color(0xFF2196F3),
Color(0xFF64B5F6)
),
onClick = { navController.navigate(Screen.Water.route) } onClick = { navController.navigate(Screen.Water.route) }
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
// Tips Card // Tips Card - Improved
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.secondaryContainer
) ),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Row(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Text(
text = "💡 Tips Kesehatan", text = "💡",
style = MaterialTheme.typography.headlineMedium
)
Column {
Text(
text = "Tips Kesehatan",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "Berjalan 10.000 langkah per hari dan minum 2 liter air dapat meningkatkan kesehatan tubuh!", text = "Berjalan 10.000 langkah per hari dan minum 2 liter air dapat meningkatkan kesehatan tubuh!",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
) )
} }
} }
} }
} }
}
}
@Composable
fun ImprovedStatCard(
icon: ImageVector,
title: String,
value: String,
unit: String,
subtitle: String,
progress: Float,
gradientColors: List<Color>,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(gradientColors)
)
.padding(20.dp)
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Column(
modifier = Modifier.weight(1f)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = CircleShape,
color = Color.White.copy(alpha = 0.2f),
modifier = Modifier.size(40.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = title,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
}
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.9f),
fontWeight = FontWeight.SemiBold
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = value,
style = MaterialTheme.typography.displaySmall,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = unit,
style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.8f),
modifier = Modifier.padding(bottom = 4.dp)
)
}
}
// Progress Circle
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.size(60.dp)
) {
CircularProgressIndicator(
progress = { progress.coerceIn(0f, 1f) },
modifier = Modifier.fillMaxSize(),
color = Color.White,
strokeWidth = 6.dp,
trackColor = Color.White.copy(alpha = 0.3f)
)
Text(
text = "${(progress * 100).toInt()}%",
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
Spacer(modifier = Modifier.height(16.dp))
LinearProgressIndicator(
progress = { progress.coerceIn(0f, 1f) },
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.clip(MaterialTheme.shapes.small),
color = Color.White,
trackColor = Color.White.copy(alpha = 0.3f),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.9f)
)
}
}
}
} }

View File

@ -12,6 +12,8 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -26,20 +28,26 @@ fun ProfileScreen(
val userName by viewModel.userName.collectAsState() val userName by viewModel.userName.collectAsState()
val stepGoal by viewModel.stepGoal.collectAsState() val stepGoal by viewModel.stepGoal.collectAsState()
val waterGoal by viewModel.waterGoal.collectAsState() val waterGoal by viewModel.waterGoal.collectAsState()
val userWeight by viewModel.userWeight.collectAsState() // TAMBAH INI
var showNameDialog by remember { mutableStateOf(false) } var showNameDialog by remember { mutableStateOf(false) }
var showStepGoalDialog by remember { mutableStateOf(false) } var showStepGoalDialog by remember { mutableStateOf(false) }
var showWaterGoalDialog by remember { mutableStateOf(false) } var showWaterGoalDialog by remember { mutableStateOf(false) }
var showWeightDialog by remember { mutableStateOf(false) } // TAMBAH INI
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text("Profil") }, title = { Text("Profil & Pengaturan") },
navigationIcon = { navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, "Kembali") Icon(Icons.Default.ArrowBack, "Kembali")
} }
} },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
) )
} }
) { paddingValues -> ) { paddingValues ->
@ -52,59 +60,126 @@ fun ProfileScreen(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
// Profile Header // Profile Header dengan Gradient
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
)
) {
Box( Box(
modifier = Modifier modifier = Modifier
.size(120.dp) .fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.primaryContainer,
MaterialTheme.colorScheme.secondaryContainer
)
)
)
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// Avatar
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer), .background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(
imageVector = Icons.Default.Person, imageVector = Icons.Default.Person,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(60.dp), modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.primary tint = MaterialTheme.colorScheme.primary
) )
} }
Text( Text(
text = userName, text = userName,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
// Stats Mini Card
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
StatsChip(
icon = Icons.Default.DirectionsWalk,
label = "Target",
value = "$stepGoal"
)
StatsChip(
icon = Icons.Default.WaterDrop,
label = "Target",
value = "${waterGoal}ml"
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Settings Section // Settings Section
Text( Text(
text = "Pengaturan", text = "Informasi Pribadi",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
// Name Setting // Name Setting
SettingItem( SettingItem(
icon = Icons.Default.Person, icon = Icons.Default.Person,
title = "Nama", title = "Nama Lengkap",
value = userName, value = userName,
iconTint = MaterialTheme.colorScheme.primary,
onClick = { showNameDialog = true } onClick = { showNameDialog = true }
) )
// TAMBAH INI - Weight Setting
SettingItem(
icon = Icons.Default.FitnessCenter,
title = "Berat Badan",
value = "$userWeight kg",
iconTint = Color(0xFFFF6B6B),
onClick = { showWeightDialog = true }
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Target Harian",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxWidth()
)
// Step Goal Setting // Step Goal Setting
SettingItem( SettingItem(
icon = Icons.Default.DirectionsWalk, icon = Icons.Default.DirectionsWalk,
title = "Target Langkah Harian", title = "Target Langkah",
value = "$stepGoal langkah", value = "$stepGoal langkah/hari",
iconTint = Color(0xFF4CAF50),
onClick = { showStepGoalDialog = true } onClick = { showStepGoalDialog = true }
) )
// Water Goal Setting // Water Goal Setting
SettingItem( SettingItem(
icon = Icons.Default.WaterDrop, icon = Icons.Default.WaterDrop,
title = "Target Air Minum Harian", title = "Target Air Minum",
value = "${waterGoal}ml", value = "${waterGoal}ml/hari",
iconTint = Color(0xFF2196F3),
onClick = { showWaterGoalDialog = true } onClick = { showWaterGoalDialog = true }
) )
@ -114,42 +189,42 @@ fun ProfileScreen(
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer containerColor = MaterialTheme.colorScheme.surfaceVariant
) )
) {
Column(
modifier = Modifier.padding(16.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) { ) {
Icon( Icon(
imageVector = Icons.Default.Info, imageVector = Icons.Default.Info,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.secondary tint = MaterialTheme.colorScheme.primary
) )
Column {
Text( Text(
text = "Tentang Aplikasi", text = "Tentang Aplikasi",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
} Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Step & Drink v1.0\nAplikasi untuk tracking langkah harian dan kebutuhan air minum.", text = "Step & Drink v1.0\nAplikasi tracking langkah harian dan kebutuhan air minum dengan perhitungan kalori.",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
} }
} }
}
// Dialogs // Dialogs
if (showNameDialog) { if (showNameDialog) {
EditTextDialog( EditTextDialog(
title = "Edit Nama", title = "Edit Nama",
label = "Nama", label = "Nama Lengkap",
currentValue = userName, currentValue = userName,
onDismiss = { showNameDialog = false }, onDismiss = { showNameDialog = false },
onConfirm = { newName -> onConfirm = { newName ->
@ -159,10 +234,24 @@ fun ProfileScreen(
) )
} }
// TAMBAH INI - Weight Dialog
if (showWeightDialog) {
EditNumberDialog(
title = "Edit Berat Badan",
label = "Berat (kg)",
currentValue = userWeight,
onDismiss = { showWeightDialog = false },
onConfirm = { newWeight ->
viewModel.updateUserWeight(newWeight)
showWeightDialog = false
}
)
}
if (showStepGoalDialog) { if (showStepGoalDialog) {
EditNumberDialog( EditNumberDialog(
title = "Edit Target Langkah", title = "Edit Target Langkah",
label = "Target (langkah)", label = "Target (langkah/hari)",
currentValue = stepGoal, currentValue = stepGoal,
onDismiss = { showStepGoalDialog = false }, onDismiss = { showStepGoalDialog = false },
onConfirm = { newGoal -> onConfirm = { newGoal ->
@ -175,7 +264,7 @@ fun ProfileScreen(
if (showWaterGoalDialog) { if (showWaterGoalDialog) {
EditNumberDialog( EditNumberDialog(
title = "Edit Target Air Minum", title = "Edit Target Air Minum",
label = "Target (ml)", label = "Target (ml/hari)",
currentValue = waterGoal, currentValue = waterGoal,
onDismiss = { showWaterGoalDialog = false }, onDismiss = { showWaterGoalDialog = false },
onConfirm = { newGoal -> onConfirm = { newGoal ->
@ -186,16 +275,56 @@ fun ProfileScreen(
} }
} }
@Composable
fun StatsChip(
icon: androidx.compose.ui.graphics.vector.ImageVector,
label: String,
value: String
) {
Surface(
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
tonalElevation = 2.dp
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
Column {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = value,
style = MaterialTheme.typography.labelMedium,
fontWeight = FontWeight.Bold
)
}
}
}
}
@Composable @Composable
fun SettingItem( fun SettingItem(
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String, title: String,
value: String, value: String,
iconTint: Color = MaterialTheme.colorScheme.primary,
onClick: () -> Unit onClick: () -> Unit
) { ) {
Card( Card(
onClick = onClick, onClick = onClick,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -206,18 +335,28 @@ fun SettingItem(
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) { ) {
Surface(
shape = CircleShape,
color = iconTint.copy(alpha = 0.1f),
modifier = Modifier.size(40.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary tint = iconTint,
modifier = Modifier.size(20.dp)
) )
}
}
Column { Column {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.Bold fontWeight = FontWeight.SemiBold
) )
Text( Text(
text = value, text = value,
@ -227,7 +366,7 @@ fun SettingItem(
} }
} }
Icon( Icon(
imageVector = Icons.Default.Edit, imageVector = Icons.Default.ChevronRight,
contentDescription = "Edit", contentDescription = "Edit",
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
@ -247,17 +386,21 @@ fun EditTextDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = {
Icon(Icons.Default.Edit, contentDescription = null)
},
title = { Text(title) }, title = { Text(title) },
text = { text = {
OutlinedTextField( OutlinedTextField(
value = text, value = text,
onValueChange = { text = it }, onValueChange = { text = it },
label = { Text(label) }, label = { Text(label) },
singleLine = true singleLine = true,
modifier = Modifier.fillMaxWidth()
) )
}, },
confirmButton = { confirmButton = {
TextButton( Button(
onClick = { onClick = {
if (text.isNotBlank()) { if (text.isNotBlank()) {
onConfirm(text) onConfirm(text)
@ -287,17 +430,21 @@ fun EditNumberDialog(
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = {
Icon(Icons.Default.Edit, contentDescription = null)
},
title = { Text(title) }, title = { Text(title) },
text = { text = {
OutlinedTextField( OutlinedTextField(
value = number, value = number,
onValueChange = { number = it.filter { char -> char.isDigit() } }, onValueChange = { number = it.filter { char -> char.isDigit() } },
label = { Text(label) }, label = { Text(label) },
singleLine = true singleLine = true,
modifier = Modifier.fillMaxWidth()
) )
}, },
confirmButton = { confirmButton = {
TextButton( Button(
onClick = { onClick = {
val value = number.toIntOrNull() val value = number.toIntOrNull()
if (value != null && value > 0) { if (value != null && value > 0) {

View File

@ -4,18 +4,22 @@ import android.Manifest
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.DirectionsWalk
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
@ -31,11 +35,17 @@ fun StepsScreen(
val stepGoal by viewModel.dailyGoal.collectAsState() val stepGoal by viewModel.dailyGoal.collectAsState()
val sensorSteps by viewModel.stepCounterManager.totalSteps.collectAsState() val sensorSteps by viewModel.stepCounterManager.totalSteps.collectAsState()
val last7Days by viewModel.last7DaysSteps.collectAsState() val last7Days by viewModel.last7DaysSteps.collectAsState()
val userWeight by viewModel.userWeight.collectAsState() // TAMBAH INI
var isTracking by remember { mutableStateOf(false) } var isTracking by remember { mutableStateOf(false) }
var permissionGranted by remember { mutableStateOf(false) } var permissionGranted by remember { mutableStateOf(false) }
// Permission launcher untuk Android 10+ // Calculate calories - TAMBAH INI
val todayCalories = remember(todaySteps?.steps, userWeight) {
viewModel.calculateCalories(todaySteps?.steps ?: 0, userWeight)
}
// Permission launcher
val permissionLauncher = rememberLauncherForActivityResult( val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted -> ) { isGranted ->
@ -54,11 +64,15 @@ fun StepsScreen(
IconButton(onClick = { navController.popBackStack() }) { IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, "Kembali") Icon(Icons.Default.ArrowBack, "Kembali")
} }
} },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
) )
}, },
floatingActionButton = { floatingActionButton = {
FloatingActionButton( ExtendedFloatingActionButton(
onClick = { onClick = {
if (isTracking) { if (isTracking) {
viewModel.stepCounterManager.stopTracking() viewModel.stepCounterManager.stopTracking()
@ -71,13 +85,16 @@ fun StepsScreen(
isTracking = true isTracking = true
} }
} }
} },
) { icon = {
Icon( Icon(
imageVector = if (isTracking) Icons.Default.Stop else Icons.Default.PlayArrow, imageVector = if (isTracking) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = if (isTracking) "Stop" else "Start" contentDescription = if (isTracking) "Stop" else "Start"
) )
} },
text = { Text(if (isTracking) "Berhenti" else "Mulai Tracking") },
containerColor = if (isTracking) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
)
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( LazyColumn(
@ -88,37 +105,56 @@ fun StepsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
item { item {
// Current Steps Card // Main Steps Card dengan Gradient
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer containerColor = Color.Transparent
) ),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(24.dp), .background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF4CAF50),
Color(0xFF81C784)
)
)
)
.padding(24.dp)
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Animated Icon
if (isTracking) {
AnimatedWalkingIcon()
} else {
Icon( Icon(
imageVector = Icons.Default.DirectionsWalk, imageVector = Icons.Default.DirectionsWalk,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(64.dp), modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary tint = Color.White
) )
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "${todaySteps?.steps ?: 0}", text = "${todaySteps?.steps ?: 0}",
style = MaterialTheme.typography.displayLarge, style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
color = Color.White
) )
Text( Text(
text = "Langkah Hari Ini", text = "Langkah Hari Ini",
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.9f)
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -129,39 +165,172 @@ fun StepsScreen(
}, },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(8.dp), .height(10.dp)
.clip(MaterialTheme.shapes.small),
color = Color.White,
trackColor = Color.White.copy(alpha = 0.3f),
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Target: $stepGoal langkah", text = "Target: $stepGoal langkah",
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.9f)
) )
if (isTracking) { if (isTracking) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(12.dp))
Badge( Surface(
containerColor = MaterialTheme.colorScheme.tertiary shape = MaterialTheme.shapes.small,
color = Color.White.copy(alpha = 0.2f)
) { ) {
Text("Sedang Melacak: $sensorSteps langkah") Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
PulsingDot()
Text(
"Sedang Tracking: $sensorSteps langkah",
color = Color.White,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
)
} }
} }
} }
} }
} }
}
}
// TAMBAH INI - Calories Card
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(
colors = listOf(
Color(0xFFFF6B6B),
Color(0xFFFF8E53)
)
)
)
.padding(20.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = "🔥",
style = MaterialTheme.typography.headlineMedium
)
}
Column {
Text(
text = "Kalori Terbakar",
style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.9f)
)
Text(
text = "$todayCalories kkal",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = Color.White
)
}
}
Column(
horizontalAlignment = Alignment.End
) {
Text(
text = "Berat: ${userWeight}kg",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.8f)
)
Text(
text = "${String.format("%.2f", todayCalories / 7.7)} gram lemak",
style = MaterialTheme.typography.bodySmall,
color = Color.White.copy(alpha = 0.8f)
)
}
}
}
}
}
// Motivational Message - TAMBAH INI
item {
val message = when {
(todaySteps?.steps ?: 0) >= stepGoal -> "🎉 Luar biasa! Target tercapai!"
(todaySteps?.steps ?: 0) >= (stepGoal * 0.75) -> "💪 Hampir sampai! Tetap semangat!"
(todaySteps?.steps ?: 0) >= (stepGoal * 0.5) -> "🚶 Setengah jalan! Ayo lanjutkan!"
(todaySteps?.steps ?: 0) >= (stepGoal * 0.25) -> "👟 Awal yang bagus! Terus bergerak!"
else -> "🌟 Yuk mulai bergerak hari ini!"
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(16.dp),
fontWeight = FontWeight.Medium
)
}
}
item { item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text( Text(
text = "Riwayat 7 Hari Terakhir", text = "Riwayat 7 Hari Terakhir",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Icon(
imageVector = Icons.Default.History,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
}
} }
items(last7Days) { record -> items(last7Days) { record ->
val recordCalories = viewModel.calculateCalories(record.steps, userWeight)
Card( Card(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@ -170,6 +339,24 @@ fun StepsScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(48.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Default.CalendarToday,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
Column { Column {
Text( Text(
text = record.date, text = record.date,
@ -177,19 +364,75 @@ fun StepsScreen(
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = "${record.steps} langkah", text = "${record.steps} langkah$recordCalories kkal",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
}
Column(
horizontalAlignment = Alignment.End
) {
CircularProgressIndicator( CircularProgressIndicator(
progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) }, progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) },
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
strokeWidth = 4.dp
) )
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${((record.steps.toFloat() / stepGoal.toFloat()) * 100).toInt()}%",
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold
)
}
} }
} }
} }
} }
} }
} }
@Composable
fun AnimatedWalkingIcon() {
val infiniteTransition = rememberInfiniteTransition(label = "walking")
val rotation by infiniteTransition.animateFloat(
initialValue = -10f,
targetValue = 10f,
animationSpec = infiniteRepeatable(
animation = tween(500, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "rotation"
)
Icon(
imageVector = Icons.Default.DirectionsWalk,
contentDescription = null,
modifier = Modifier
.size(64.dp)
.rotate(rotation),
tint = Color.White
)
}
@Composable
fun PulsingDot() {
val infiniteTransition = rememberInfiniteTransition(label = "pulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 0.3f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = alpha))
)
}

View File

@ -22,6 +22,9 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
val waterGoal: StateFlow<Int> = preferencesManager.waterGoal val waterGoal: StateFlow<Int> = preferencesManager.waterGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
val userWeight: StateFlow<Int> = preferencesManager.userWeight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 70)
fun updateUserName(name: String) { fun updateUserName(name: String) {
viewModelScope.launch { viewModelScope.launch {
preferencesManager.saveUserName(name) preferencesManager.saveUserName(name)
@ -39,4 +42,11 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
preferencesManager.saveWaterGoal(goal) preferencesManager.saveWaterGoal(goal)
} }
} }
fun updateUserWeight(weight: Int) {
viewModelScope.launch {
preferencesManager.saveUserWeight(weight)
}
}
} }

View File

@ -3,34 +3,46 @@ package com.example.stepdrink.viewmodel
import android.app.Application import android.app.Application
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.stepdrink.data.local.PreferencesManager
import com.example.stepdrink.data.local.database.AppDatabase import com.example.stepdrink.data.local.database.AppDatabase
import com.example.stepdrink.data.local.entity.StepRecord import com.example.stepdrink.data.local.entity.StepRecord
import com.example.stepdrink.data.repository.StepRepository import com.example.stepdrink.data.repository.StepRepository
import com.example.stepdrink.sensor.StepCounterManager import com.example.stepdrink.sensor.StepCounterManager
import com.example.stepdrink.data.local.PreferencesManager
import com.example.stepdrink.util.DateUtils import com.example.stepdrink.util.DateUtils
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class StepViewModel(application: Application) : AndroidViewModel(application) { class StepViewModel(application: Application) : AndroidViewModel(application) {
private val repository: StepRepository = StepRepository( // PERBAIKAN: Initialize repository dulu di init block
AppDatabase.getDatabase(application).stepDao() private val repository: StepRepository
) val stepCounterManager: StepCounterManager
val stepCounterManager: StepCounterManager = StepCounterManager(application)
private val preferencesManager = PreferencesManager(application) private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = preferencesManager.stepGoal val dailyGoal: StateFlow<Int>
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000) val userWeight: StateFlow<Int>
val todaySteps: StateFlow<StepRecord?>
val todaySteps: StateFlow<StepRecord?> = repository.getStepsByDate(DateUtils.getCurrentDate()) val last7DaysSteps: StateFlow<List<StepRecord>>
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
val last7DaysSteps: StateFlow<List<StepRecord>> = repository.getLast7DaysSteps()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
init { init {
// Initialize database & repository
val database = AppDatabase.getDatabase(application)
repository = StepRepository(database.stepDao())
stepCounterManager = StepCounterManager(application)
// Initialize flows
dailyGoal = preferencesManager.stepGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000)
userWeight = preferencesManager.userWeight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 70)
todaySteps = repository.getStepsByDate(DateUtils.getCurrentDate())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
last7DaysSteps = repository.getLast7DaysSteps()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
// Observe sensor dan update database // Observe sensor dan update database
viewModelScope.launch { viewModelScope.launch {
stepCounterManager.totalSteps.collect { steps -> stepCounterManager.totalSteps.collect { steps ->
@ -47,6 +59,25 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
fun getProgressPercentage(): Float {
val today = todaySteps.value?.steps ?: 0
return (today.toFloat() / dailyGoal.value.toFloat()).coerceIn(0f, 1f)
}
// Function untuk hitung kalori
fun calculateCalories(steps: Int, weight: Int): Int {
// Rumus: (Steps × Weight × 0.57) / 1000
// Contoh: 10000 steps × 70kg = 399 kalori
return ((steps * weight * 0.57) / 1000).toInt()
}
// Get kalori untuk hari ini
fun getTodayCalories(): Int {
val steps = todaySteps.value?.steps ?: 0
val weight = userWeight.value
return calculateCalories(steps, weight)
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
stepCounterManager.stopTracking() stepCounterManager.stopTracking()