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 STEP_GOAL = intPreferencesKey("step_goal")
val WATER_GOAL = intPreferencesKey("water_goal")
val USER_WEIGHT = intPreferencesKey("user_weight")
}
val userName: Flow<String> = context.dataStore.data.map { preferences ->
@ -32,6 +33,10 @@ class PreferencesManager(private val context: Context) {
preferences[WATER_GOAL] ?: 2000
}
val userWeight: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[USER_WEIGHT] ?: 70
}
suspend fun saveUserName(name: String) {
context.dataStore.edit { preferences ->
preferences[USER_NAME] = name
@ -49,4 +54,9 @@ class PreferencesManager(private val context: Context) {
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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DirectionsWalk
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.unit.dp
import androidx.navigation.NavController
import com.example.stepdrink.ui.components.StatCard
import com.example.stepdrink.ui.navigation.Screen
import com.example.stepdrink.viewmodel.StepViewModel
import com.example.stepdrink.viewmodel.WaterViewModel
import com.example.stepdrink.viewmodel.ProfileViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
@ -36,14 +41,44 @@ fun HomeScreen(
topBar = {
TopAppBar(
title = {
Text(
text = "Step & Drink",
fontWeight = FontWeight.Bold
)
},
Row(
verticalAlignment = Alignment.CenterVertically,
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 = {
IconButton(onClick = { navController.navigate(Screen.Profile.route) }) {
Icon(Icons.Default.Person, "Profil")
IconButton(
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(
@ -60,61 +95,241 @@ fun HomeScreen(
.padding(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 = "Halo, $userName! 👋",
text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
// Steps Card
StatCard(
// Steps Card - Improved
ImprovedStatCard(
icon = Icons.Default.DirectionsWalk,
title = "Langkah",
value = "${todaySteps?.steps ?: 0}",
unit = "langkah",
subtitle = "Target: $stepGoal langkah",
progress = (todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat(),
gradientColors = listOf(
Color(0xFF4CAF50),
Color(0xFF81C784)
),
onClick = { navController.navigate(Screen.Steps.route) }
)
// Water Card
StatCard(
// Water Card - Improved
ImprovedStatCard(
icon = Icons.Default.WaterDrop,
title = "Air Minum",
value = "${todayWater}ml",
value = "$todayWater",
unit = "ml",
subtitle = "Target: ${waterGoal}ml",
progress = todayWater.toFloat() / waterGoal.toFloat(),
gradientColors = listOf(
Color(0xFF2196F3),
Color(0xFF64B5F6)
),
onClick = { navController.navigate(Screen.Water.route) }
)
Spacer(modifier = Modifier.weight(1f))
// Tips Card
// Tips Card - Improved
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "💡 Tips Kesehatan",
style = MaterialTheme.typography.titleMedium,
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
text = "💡",
style = MaterialTheme.typography.headlineMedium
)
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.Modifier
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.unit.dp
import androidx.navigation.NavController
@ -26,20 +28,26 @@ fun ProfileScreen(
val userName by viewModel.userName.collectAsState()
val stepGoal by viewModel.stepGoal.collectAsState()
val waterGoal by viewModel.waterGoal.collectAsState()
val userWeight by viewModel.userWeight.collectAsState() // TAMBAH INI
var showNameDialog by remember { mutableStateOf(false) }
var showStepGoalDialog by remember { mutableStateOf(false) }
var showWaterGoalDialog by remember { mutableStateOf(false) }
var showWeightDialog by remember { mutableStateOf(false) } // TAMBAH INI
Scaffold(
topBar = {
TopAppBar(
title = { Text("Profil") },
title = { Text("Profil & Pengaturan") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, "Kembali")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
@ -52,59 +60,126 @@ fun ProfileScreen(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Profile Header
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.primary
// Profile Header dengan Gradient
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = Color.Transparent
)
}
) {
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 = userName,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Text(
text = userName,
style = MaterialTheme.typography.headlineMedium,
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))
// Settings Section
Text(
text = "Pengaturan",
style = MaterialTheme.typography.titleLarge,
text = "Informasi Pribadi",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.fillMaxWidth()
)
// Name Setting
SettingItem(
icon = Icons.Default.Person,
title = "Nama",
title = "Nama Lengkap",
value = userName,
iconTint = MaterialTheme.colorScheme.primary,
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
SettingItem(
icon = Icons.Default.DirectionsWalk,
title = "Target Langkah Harian",
value = "$stepGoal langkah",
title = "Target Langkah",
value = "$stepGoal langkah/hari",
iconTint = Color(0xFF4CAF50),
onClick = { showStepGoalDialog = true }
)
// Water Goal Setting
SettingItem(
icon = Icons.Default.WaterDrop,
title = "Target Air Minum Harian",
value = "${waterGoal}ml",
title = "Target Air Minum",
value = "${waterGoal}ml/hari",
iconTint = Color(0xFF2196F3),
onClick = { showWaterGoalDialog = true }
)
@ -114,32 +189,32 @@ fun ProfileScreen(
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier.padding(16.dp)
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Column {
Text(
text = "Tentang Aplikasi",
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleSmall,
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) {
EditTextDialog(
title = "Edit Nama",
label = "Nama",
label = "Nama Lengkap",
currentValue = userName,
onDismiss = { showNameDialog = false },
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) {
EditNumberDialog(
title = "Edit Target Langkah",
label = "Target (langkah)",
label = "Target (langkah/hari)",
currentValue = stepGoal,
onDismiss = { showStepGoalDialog = false },
onConfirm = { newGoal ->
@ -175,7 +264,7 @@ fun ProfileScreen(
if (showWaterGoalDialog) {
EditNumberDialog(
title = "Edit Target Air Minum",
label = "Target (ml)",
label = "Target (ml/hari)",
currentValue = waterGoal,
onDismiss = { showWaterGoalDialog = false },
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
fun SettingItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
title: String,
value: String,
iconTint: Color = MaterialTheme.colorScheme.primary,
onClick: () -> Unit
) {
Card(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
@ -206,18 +335,28 @@ fun SettingItem(
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Surface(
shape = CircleShape,
color = iconTint.copy(alpha = 0.1f),
modifier = Modifier.size(40.dp)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(20.dp)
)
}
}
Column {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold
)
Text(
text = value,
@ -227,7 +366,7 @@ fun SettingItem(
}
}
Icon(
imageVector = Icons.Default.Edit,
imageVector = Icons.Default.ChevronRight,
contentDescription = "Edit",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
@ -247,17 +386,21 @@ fun EditTextDialog(
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(Icons.Default.Edit, contentDescription = null)
},
title = { Text(title) },
text = {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text(label) },
singleLine = true
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(
Button(
onClick = {
if (text.isNotBlank()) {
onConfirm(text)
@ -287,17 +430,21 @@ fun EditNumberDialog(
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(Icons.Default.Edit, contentDescription = null)
},
title = { Text(title) },
text = {
OutlinedTextField(
value = number,
onValueChange = { number = it.filter { char -> char.isDigit() } },
label = { Text(label) },
singleLine = true
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(
Button(
onClick = {
val value = number.toIntOrNull()
if (value != null && value > 0) {

View File

@ -4,18 +4,22 @@ import android.Manifest
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.DirectionsWalk
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
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.unit.dp
import androidx.navigation.NavController
@ -31,11 +35,17 @@ fun StepsScreen(
val stepGoal by viewModel.dailyGoal.collectAsState()
val sensorSteps by viewModel.stepCounterManager.totalSteps.collectAsState()
val last7Days by viewModel.last7DaysSteps.collectAsState()
val userWeight by viewModel.userWeight.collectAsState() // TAMBAH INI
var isTracking 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(
ActivityResultContracts.RequestPermission()
) { isGranted ->
@ -54,11 +64,15 @@ fun StepsScreen(
IconButton(onClick = { navController.popBackStack() }) {
Icon(Icons.Default.ArrowBack, "Kembali")
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
},
floatingActionButton = {
FloatingActionButton(
ExtendedFloatingActionButton(
onClick = {
if (isTracking) {
viewModel.stepCounterManager.stopTracking()
@ -71,13 +85,16 @@ fun StepsScreen(
isTracking = true
}
}
}
) {
Icon(
imageVector = if (isTracking) Icons.Default.Stop else Icons.Default.PlayArrow,
contentDescription = if (isTracking) "Stop" else "Start"
)
}
},
icon = {
Icon(
imageVector = if (isTracking) Icons.Default.Stop else Icons.Default.PlayArrow,
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 ->
LazyColumn(
@ -88,80 +105,232 @@ fun StepsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
// Current Steps Card
// Main Steps Card dengan Gradient
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
containerColor = Color.Transparent
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Column(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF4CAF50),
Color(0xFF81C784)
)
)
)
.padding(24.dp)
) {
Icon(
imageVector = Icons.Default.DirectionsWalk,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary
)
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 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 = "${todaySteps?.steps ?: 0}",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold
)
Text(
text = "${todaySteps?.steps ?: 0}",
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = Color.White
)
Text(
text = "Langkah Hari Ini",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "Langkah Hari Ini",
style = MaterialTheme.typography.titleMedium,
color = Color.White.copy(alpha = 0.9f)
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(16.dp))
LinearProgressIndicator(
progress = {
(todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat()
},
modifier = Modifier
.fillMaxWidth()
.height(8.dp),
)
LinearProgressIndicator(
progress = {
(todaySteps?.steps ?: 0).toFloat() / stepGoal.toFloat()
},
modifier = Modifier
.fillMaxWidth()
.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))
Badge(
containerColor = MaterialTheme.colorScheme.tertiary
) {
Text("Sedang Melacak: $sensorSteps langkah")
Text(
text = "Target: $stepGoal 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 {
Text(
text = "Riwayat 7 Hari Terakhir",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
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 {
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 ->
val recordCalories = viewModel.calculateCalories(record.steps, userWeight)
Card(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier
@ -170,26 +339,100 @@ fun StepsScreen(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = record.date,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "${record.steps} langkah",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
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 {
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(
progress = { (record.steps.toFloat() / stepGoal.toFloat()).coerceIn(0f, 1f) },
modifier = Modifier.size(48.dp),
)
Column(
horizontalAlignment = Alignment.End
) {
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
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
val userWeight: StateFlow<Int> = preferencesManager.userWeight
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 70)
fun updateUserName(name: String) {
viewModelScope.launch {
preferencesManager.saveUserName(name)
@ -39,4 +42,11 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application)
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 androidx.lifecycle.AndroidViewModel
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.entity.StepRecord
import com.example.stepdrink.data.repository.StepRepository
import com.example.stepdrink.sensor.StepCounterManager
import com.example.stepdrink.data.local.PreferencesManager
import com.example.stepdrink.util.DateUtils
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class StepViewModel(application: Application) : AndroidViewModel(application) {
private val repository: StepRepository = StepRepository(
AppDatabase.getDatabase(application).stepDao()
)
val stepCounterManager: StepCounterManager = StepCounterManager(application)
// PERBAIKAN: Initialize repository dulu di init block
private val repository: StepRepository
val stepCounterManager: StepCounterManager
private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = preferencesManager.stepGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000)
val todaySteps: StateFlow<StepRecord?> = repository.getStepsByDate(DateUtils.getCurrentDate())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
val last7DaysSteps: StateFlow<List<StepRecord>> = repository.getLast7DaysSteps()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val dailyGoal: StateFlow<Int>
val userWeight: StateFlow<Int>
val todaySteps: StateFlow<StepRecord?>
val last7DaysSteps: StateFlow<List<StepRecord>>
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
viewModelScope.launch {
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() {
super.onCleared()
stepCounterManager.stopTracking()