diff --git a/app/src/main/java/com/example/stepdrink/MainActivity.kt b/app/src/main/java/com/example/stepdrink/MainActivity.kt index c361896..3382dab 100644 --- a/app/src/main/java/com/example/stepdrink/MainActivity.kt +++ b/app/src/main/java/com/example/stepdrink/MainActivity.kt @@ -14,7 +14,7 @@ import com.example.stepdrink.ui.navigation.NavGraph import com.example.stepdrink.ui.theme.StepDrinkTheme import com.example.stepdrink.viewmodel.StepViewModel import com.example.stepdrink.viewmodel.WaterViewModel - +import com.example.stepdrink.viewmodel.ProfileViewModel class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,11 +28,13 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() val stepViewModel: StepViewModel = viewModel() val waterViewModel: WaterViewModel = viewModel() + val profileViewModel: ProfileViewModel = viewModel() NavGraph( navController = navController, stepViewModel = stepViewModel, - waterViewModel = waterViewModel + waterViewModel = waterViewModel, + profileViewModel = profileViewModel ) } } diff --git a/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt b/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt new file mode 100644 index 0000000..1666cae --- /dev/null +++ b/app/src/main/java/data/local/prefrencesmanager/PrefrencesManager.kt @@ -0,0 +1,52 @@ +package com.example.stepdrink.data.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +private val Context.dataStore: DataStore by preferencesDataStore(name = "settings") + +class PreferencesManager(private val context: Context) { + + companion object { + val USER_NAME = stringPreferencesKey("user_name") + val STEP_GOAL = intPreferencesKey("step_goal") + val WATER_GOAL = intPreferencesKey("water_goal") + } + + val userName: Flow = context.dataStore.data.map { preferences -> + preferences[USER_NAME] ?: "Pengguna" + } + + val stepGoal: Flow = context.dataStore.data.map { preferences -> + preferences[STEP_GOAL] ?: 10000 + } + + val waterGoal: Flow = context.dataStore.data.map { preferences -> + preferences[WATER_GOAL] ?: 2000 + } + + suspend fun saveUserName(name: String) { + context.dataStore.edit { preferences -> + preferences[USER_NAME] = name + } + } + + suspend fun saveStepGoal(goal: Int) { + context.dataStore.edit { preferences -> + preferences[STEP_GOAL] = goal + } + } + + suspend fun saveWaterGoal(goal: Int) { + context.dataStore.edit { preferences -> + preferences[WATER_GOAL] = goal + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ui/navigation/NavGraph.kt b/app/src/main/java/ui/navigation/NavGraph.kt index 4afc0d2..79a2a49 100644 --- a/app/src/main/java/ui/navigation/NavGraph.kt +++ b/app/src/main/java/ui/navigation/NavGraph.kt @@ -7,14 +7,17 @@ import androidx.navigation.compose.composable import com.example.stepdrink.ui.screen.home.HomeScreen import com.example.stepdrink.ui.screen.steps.StepsScreen import com.example.stepdrink.ui.screen.water.WaterScreen +import com.example.stepdrink.ui.screen.profile.ProfileScreen import com.example.stepdrink.viewmodel.StepViewModel import com.example.stepdrink.viewmodel.WaterViewModel +import com.example.stepdrink.viewmodel.ProfileViewModel @Composable fun NavGraph( navController: NavHostController, stepViewModel: StepViewModel, - waterViewModel: WaterViewModel + waterViewModel: WaterViewModel, + profileViewModel: ProfileViewModel ) { NavHost( navController = navController, @@ -24,7 +27,8 @@ fun NavGraph( HomeScreen( navController = navController, stepViewModel = stepViewModel, - waterViewModel = waterViewModel + waterViewModel = waterViewModel, + profileViewModel = profileViewModel ) } @@ -41,5 +45,11 @@ fun NavGraph( viewModel = waterViewModel ) } + composable(Screen.Profile.route) { + ProfileScreen( + navController = navController, + viewModel = profileViewModel ) } +} + } \ No newline at end of file diff --git a/app/src/main/java/ui/navigation/Screen.kt b/app/src/main/java/ui/navigation/Screen.kt index 2ef4f3b..14ef829 100644 --- a/app/src/main/java/ui/navigation/Screen.kt +++ b/app/src/main/java/ui/navigation/Screen.kt @@ -4,4 +4,5 @@ sealed class Screen(val route: String) { object Home : Screen("home") object Steps : Screen("steps") object Water : Screen("water") + object Profile : Screen("profile") } \ 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 26b2668..fa611e5 100644 --- a/app/src/main/java/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/ui/screen/home/HomeScreen.kt @@ -3,6 +3,7 @@ package com.example.stepdrink.ui.screen.home import androidx.compose.foundation.layout.* 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.* @@ -14,18 +15,22 @@ 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( navController: NavController, stepViewModel: StepViewModel, - waterViewModel: WaterViewModel + waterViewModel: WaterViewModel, + profileViewModel: ProfileViewModel ) { val todaySteps by stepViewModel.todaySteps.collectAsState() val stepGoal by stepViewModel.dailyGoal.collectAsState() val todayWater by waterViewModel.todayTotalWater.collectAsState() val waterGoal by waterViewModel.dailyGoal.collectAsState() + val userName by profileViewModel.userName.collectAsState() Scaffold( topBar = { @@ -35,6 +40,11 @@ fun HomeScreen( text = "Step & Drink", fontWeight = FontWeight.Bold ) + }, + actions = { + IconButton(onClick = { navController.navigate(Screen.Profile.route) }) { + Icon(Icons.Default.Person, "Profil") + } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, @@ -50,6 +60,11 @@ fun HomeScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + Text( + text = "Halo, $userName! 👋", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) Text( text = "Aktivitas Hari Ini", style = MaterialTheme.typography.headlineSmall, diff --git a/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt b/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt new file mode 100644 index 0000000..82c86e8 --- /dev/null +++ b/app/src/main/java/ui/screen/profilescreen/ProfileScreen.kt @@ -0,0 +1,317 @@ +package com.example.stepdrink.ui.screen.profile + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +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.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import com.example.stepdrink.viewmodel.ProfileViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ProfileScreen( + navController: NavController, + viewModel: ProfileViewModel +) { + val userName by viewModel.userName.collectAsState() + val stepGoal by viewModel.stepGoal.collectAsState() + val waterGoal by viewModel.waterGoal.collectAsState() + + var showNameDialog by remember { mutableStateOf(false) } + var showStepGoalDialog by remember { mutableStateOf(false) } + var showWaterGoalDialog by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Profil") }, + navigationIcon = { + IconButton(onClick = { navController.popBackStack() }) { + Icon(Icons.Default.ArrowBack, "Kembali") + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + 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 + ) + } + + Text( + text = userName, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Settings Section + Text( + text = "Pengaturan", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.fillMaxWidth() + ) + + // Name Setting + SettingItem( + icon = Icons.Default.Person, + title = "Nama", + value = userName, + onClick = { showNameDialog = true } + ) + + // Step Goal Setting + SettingItem( + icon = Icons.Default.DirectionsWalk, + title = "Target Langkah Harian", + value = "$stepGoal langkah", + onClick = { showStepGoalDialog = true } + ) + + // Water Goal Setting + SettingItem( + icon = Icons.Default.WaterDrop, + title = "Target Air Minum Harian", + value = "${waterGoal}ml", + onClick = { showWaterGoalDialog = true } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Info Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary + ) + Text( + text = "Tentang Aplikasi", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + } + 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 + ) + } + } + } + } + + // Dialogs + if (showNameDialog) { + EditTextDialog( + title = "Edit Nama", + label = "Nama", + currentValue = userName, + onDismiss = { showNameDialog = false }, + onConfirm = { newName -> + viewModel.updateUserName(newName) + showNameDialog = false + } + ) + } + + if (showStepGoalDialog) { + EditNumberDialog( + title = "Edit Target Langkah", + label = "Target (langkah)", + currentValue = stepGoal, + onDismiss = { showStepGoalDialog = false }, + onConfirm = { newGoal -> + viewModel.updateStepGoal(newGoal) + showStepGoalDialog = false + } + ) + } + + if (showWaterGoalDialog) { + EditNumberDialog( + title = "Edit Target Air Minum", + label = "Target (ml)", + currentValue = waterGoal, + onDismiss = { showWaterGoalDialog = false }, + onConfirm = { newGoal -> + viewModel.updateWaterGoal(newGoal) + showWaterGoalDialog = false + } + ) + } +} + +@Composable +fun SettingItem( + icon: androidx.compose.ui.graphics.vector.ImageVector, + title: String, + value: String, + onClick: () -> Unit +) { + Card( + onClick = onClick, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Column { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +fun EditTextDialog( + title: String, + label: String, + currentValue: String, + onDismiss: () -> Unit, + onConfirm: (String) -> Unit +) { + var text by remember { mutableStateOf(currentValue) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + OutlinedTextField( + value = text, + onValueChange = { text = it }, + label = { Text(label) }, + singleLine = true + ) + }, + confirmButton = { + TextButton( + onClick = { + if (text.isNotBlank()) { + onConfirm(text) + } + } + ) { + Text("Simpan") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Batal") + } + } + ) +} + +@Composable +fun EditNumberDialog( + title: String, + label: String, + currentValue: Int, + onDismiss: () -> Unit, + onConfirm: (Int) -> Unit +) { + var number by remember { mutableStateOf(currentValue.toString()) } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + OutlinedTextField( + value = number, + onValueChange = { number = it.filter { char -> char.isDigit() } }, + label = { Text(label) }, + singleLine = true + ) + }, + confirmButton = { + TextButton( + onClick = { + val value = number.toIntOrNull() + if (value != null && value > 0) { + onConfirm(value) + } + } + ) { + Text("Simpan") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Batal") + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/viewmodel/ProfileViewModel.kt b/app/src/main/java/viewmodel/ProfileViewModel.kt new file mode 100644 index 0000000..372c1ec --- /dev/null +++ b/app/src/main/java/viewmodel/ProfileViewModel.kt @@ -0,0 +1,42 @@ +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 kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class ProfileViewModel(application: Application) : AndroidViewModel(application) { + + private val preferencesManager = PreferencesManager(application) + + val userName: StateFlow = preferencesManager.userName + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "Pengguna") + + val stepGoal: StateFlow = preferencesManager.stepGoal + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000) + + val waterGoal: StateFlow = preferencesManager.waterGoal + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000) + + fun updateUserName(name: String) { + viewModelScope.launch { + preferencesManager.saveUserName(name) + } + } + + fun updateStepGoal(goal: Int) { + viewModelScope.launch { + preferencesManager.saveStepGoal(goal) + } + } + + fun updateWaterGoal(goal: Int) { + viewModelScope.launch { + preferencesManager.saveWaterGoal(goal) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/viewmodel/StepViewModel.kt b/app/src/main/java/viewmodel/StepViewModel.kt index 1fcca26..ae1a25c 100644 --- a/app/src/main/java/viewmodel/StepViewModel.kt +++ b/app/src/main/java/viewmodel/StepViewModel.kt @@ -7,6 +7,7 @@ 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 @@ -18,8 +19,10 @@ class StepViewModel(application: Application) : AndroidViewModel(application) { ) val stepCounterManager: StepCounterManager = StepCounterManager(application) - private val _dailyGoal = MutableStateFlow(10000) - val dailyGoal: StateFlow = _dailyGoal.asStateFlow() + 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) @@ -44,15 +47,6 @@ class StepViewModel(application: Application) : AndroidViewModel(application) { } } - fun setDailyGoal(goal: Int) { - _dailyGoal.value = goal - } - - fun getProgressPercentage(): Float { - val today = todaySteps.value?.steps ?: 0 - return (today.toFloat() / _dailyGoal.value.toFloat()).coerceIn(0f, 1f) - } - override fun onCleared() { super.onCleared() stepCounterManager.stopTracking() diff --git a/app/src/main/java/viewmodel/WaterViewModel.kt b/app/src/main/java/viewmodel/WaterViewModel.kt index 1e7261c..017bac0 100644 --- a/app/src/main/java/viewmodel/WaterViewModel.kt +++ b/app/src/main/java/viewmodel/WaterViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import com.example.stepdrink.data.local.database.AppDatabase +import com.example.stepdrink.data.local.PreferencesManager import com.example.stepdrink.data.local.entity.WaterRecord import com.example.stepdrink.data.repository.WaterRepository import com.example.stepdrink.util.DateUtils @@ -16,8 +17,10 @@ class WaterViewModel(application: Application) : AndroidViewModel(application) { AppDatabase.getDatabase(application).waterDao() ) - private val _dailyGoal = MutableStateFlow(2000) // 2000ml = 2 liter - val dailyGoal: StateFlow = _dailyGoal.asStateFlow() + private val preferencesManager = PreferencesManager(application) + + val dailyGoal: StateFlow = preferencesManager.waterGoal + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000) val todayWaterRecords: StateFlow> = repository.getWaterRecordsByDate(DateUtils.getCurrentDate()) @@ -43,12 +46,4 @@ class WaterViewModel(application: Application) : AndroidViewModel(application) { repository.deleteWaterRecord(record) } } - - fun setDailyGoal(goal: Int) { - _dailyGoal.value = goal - } - - fun getProgressPercentage(): Float { - return (todayTotalWater.value.toFloat() / _dailyGoal.value.toFloat()).coerceIn(0f, 1f) - } } \ No newline at end of file