ui ux update, adding calorie counter
This commit is contained in:
parent
edbb6a52fb
commit
a23005280b
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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,15 +364,26 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,3 +391,48 @@ fun StepsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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))
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user