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 = {
Text( Row(
text = "Step & Drink", verticalAlignment = Alignment.CenterVertically,
fontWeight = FontWeight.Bold horizontalArrangement = Arrangement.spacedBy(8.dp)
) ) {
}, Text(
text = "Step",
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 = "Halo, $userName! 👋",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Text(
text = "Tetap sehat dengan tracking harian",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Text( Text(
text = "Halo, $userName! 👋", text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text(
text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
// Steps Card // Steps Card - Improved
StatCard( 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.titleMedium, style = MaterialTheme.typography.headlineMedium
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Berjalan 10.000 langkah per hari dan minum 2 liter air dapat meningkatkan kesehatan tubuh!",
style = MaterialTheme.typography.bodyMedium
) )
Column {
Text(
text = "Tips Kesehatan",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Berjalan 10.000 langkah per hari dan minum 2 liter air dapat meningkatkan kesehatan tubuh!",
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
Box( Card(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.size(120.dp) colors = CardDefaults.cardColors(
.clip(CircleShape) containerColor = Color.Transparent
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.primary
) )
} ) {
Box(
modifier = Modifier
.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)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(50.dp),
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,32 +189,32 @@ 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( Row(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) { ) {
Row( Icon(
verticalAlignment = Alignment.CenterVertically, imageVector = Icons.Default.Info,
horizontalArrangement = Arrangement.spacedBy(8.dp) contentDescription = null,
) { tint = MaterialTheme.colorScheme.primary
Icon( )
imageVector = Icons.Default.Info, Column {
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
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))
Text(
text = "Step & Drink v1.0\nAplikasi tracking langkah harian dan kebutuhan air minum dengan perhitungan kalori.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Step & Drink v1.0\nAplikasi untuk tracking langkah harian dan kebutuhan air minum.",
style = MaterialTheme.typography.bodyMedium
)
} }
} }
} }
@ -149,7 +224,7 @@ fun ProfileScreen(
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)
) { ) {
Icon( Surface(
imageVector = icon, shape = CircleShape,
contentDescription = null, color = iconTint.copy(alpha = 0.1f),
tint = MaterialTheme.colorScheme.primary modifier = Modifier.size(40.dp)
) ) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
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,80 +105,232 @@ 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(
horizontalAlignment = Alignment.CenterHorizontally brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF4CAF50),
Color(0xFF81C784)
)
)
)
.padding(24.dp)
) { ) {
Icon( Column(
imageVector = Icons.Default.DirectionsWalk, modifier = Modifier.fillMaxWidth(),
contentDescription = null, horizontalAlignment = Alignment.CenterHorizontally
modifier = Modifier.size(64.dp), ) {
tint = MaterialTheme.colorScheme.primary // Animated Icon
) if (isTracking) {
AnimatedWalkingIcon()
} else {
Icon(
imageVector = Icons.Default.DirectionsWalk,
contentDescription = null,
modifier = Modifier.size(64.dp),
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))
LinearProgressIndicator( LinearProgressIndicator(
progress = { progress = {
(todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat() (todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat()
}, },
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))
Text(
text = "Target: $stepGoal langkah",
style = MaterialTheme.typography.bodyMedium
)
if (isTracking) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Badge(
containerColor = MaterialTheme.colorScheme.tertiary Text(
) { text = "Target: $stepGoal langkah",
Text("Sedang Melacak: $sensorSteps langkah") style = MaterialTheme.typography.bodyMedium,
color = Color.White.copy(alpha = 0.9f)
)
if (isTracking) {
Spacer(modifier = Modifier.height(12.dp))
Surface(
shape = MaterialTheme.shapes.small,
color = Color.White.copy(alpha = 0.2f)
) {
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 { item {
Text( Card(
text = "Riwayat 7 Hari Terakhir", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.titleLarge, colors = CardDefaults.cardColors(
fontWeight = FontWeight.Bold 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 {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Riwayat 7 Hari Terakhir",
style = MaterialTheme.typography.titleLarge,
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,26 +339,100 @@ fun StepsScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column { Row(
Text( horizontalArrangement = Arrangement.spacedBy(12.dp),
text = record.date, verticalAlignment = Alignment.CenterVertically
style = MaterialTheme.typography.titleMedium, ) {
fontWeight = FontWeight.Bold Surface(
) shape = CircleShape,
Text( color = MaterialTheme.colorScheme.primaryContainer,
text = "${record.steps} langkah", modifier = Modifier.size(48.dp)
style = MaterialTheme.typography.bodyMedium, ) {
color = MaterialTheme.colorScheme.onSurfaceVariant Box(contentAlignment = Alignment.Center) {
) Icon(
imageVector = Icons.Default.CalendarToday,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
}
Column {
Text(
text = record.date,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "${record.steps} langkah • $recordCalories kkal",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
CircularProgressIndicator( Column(
progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) }, horizontalAlignment = Alignment.End
modifier = Modifier.size(48.dp), ) {
) CircularProgressIndicator(
progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) },
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()