diff --git a/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt b/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt index 1666cae..eda7677 100644 --- a/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt +++ b/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt @@ -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 = context.dataStore.data.map { preferences -> @@ -32,6 +33,10 @@ class PreferencesManager(private val context: Context) { preferences[WATER_GOAL] ?: 2000 } + val userWeight: Flow = 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/ui/screen/home/HomeScreen.kt b/app/src/main/java/ui/screen/home/HomeScreen.kt index fa611e5..61f1f3b 100644 --- a/app/src/main/java/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/ui/screen/home/HomeScreen.kt @@ -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, + 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) + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt b/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt index 82c86e8..a122934 100644 --- a/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt +++ b/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt @@ -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) { diff --git a/app/src/main/java/ui/screen/steps/StepsScreen.kt b/app/src/main/java/ui/screen/steps/StepsScreen.kt index 52b2e1f..86912f7 100644 --- a/app/src/main/java/ui/screen/steps/StepsScreen.kt +++ b/app/src/main/java/ui/screen/steps/StepsScreen.kt @@ -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)) + ) } \ No newline at end of file diff --git a/app/src/main/java/viewmodel/ProfileViewModel.kt b/app/src/main/java/viewmodel/ProfileViewModel.kt index 372c1ec..9bdbca6 100644 --- a/app/src/main/java/viewmodel/ProfileViewModel.kt +++ b/app/src/main/java/viewmodel/ProfileViewModel.kt @@ -22,6 +22,9 @@ class ProfileViewModel(application: Application) : AndroidViewModel(application) val waterGoal: StateFlow = preferencesManager.waterGoal .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000) + val userWeight: StateFlow = 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) } } -} \ No newline at end of file + fun updateUserWeight(weight: Int) { + viewModelScope.launch { + preferencesManager.saveUserWeight(weight) + + } + } +} + diff --git a/app/src/main/java/viewmodel/StepViewModel.kt b/app/src/main/java/viewmodel/StepViewModel.kt index ae1a25c..c6cdd4c 100644 --- a/app/src/main/java/viewmodel/StepViewModel.kt +++ b/app/src/main/java/viewmodel/StepViewModel.kt @@ -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 = preferencesManager.stepGoal - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000) - - val todaySteps: StateFlow = repository.getStepsByDate(DateUtils.getCurrentDate()) - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) - - val last7DaysSteps: StateFlow> = repository.getLast7DaysSteps() - .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) + val dailyGoal: StateFlow + val userWeight: StateFlow + val todaySteps: StateFlow + val last7DaysSteps: StateFlow> 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()