update add ing profile menu

This commit is contained in:
HagaDalpintoGinting 2025-12-16 23:47:16 +07:00
parent bc63ed83bb
commit 3db5b052a7
9 changed files with 454 additions and 26 deletions

View File

@ -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
)
}
}

View File

@ -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<Preferences> 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<String> = context.dataStore.data.map { preferences ->
preferences[USER_NAME] ?: "Pengguna"
}
val stepGoal: Flow<Int> = context.dataStore.data.map { preferences ->
preferences[STEP_GOAL] ?: 10000
}
val waterGoal: Flow<Int> = 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
}
}
}

View File

@ -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 )
}
}
}

View File

@ -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")
}

View File

@ -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 = {
@ -36,6 +41,11 @@ fun HomeScreen(
fontWeight = FontWeight.Bold
)
},
actions = {
IconButton(onClick = { navController.navigate(Screen.Profile.route) }) {
Icon(Icons.Default.Person, "Profil")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
@ -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,

View File

@ -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")
}
}
)
}

View File

@ -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<String> = preferencesManager.userName
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "Pengguna")
val stepGoal: StateFlow<Int> = preferencesManager.stepGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000)
val waterGoal: StateFlow<Int> = 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)
}
}
}

View File

@ -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<Int> = _dailyGoal.asStateFlow()
private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = preferencesManager.stepGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000)
val todaySteps: StateFlow<StepRecord?> = repository.getStepsByDate(DateUtils.getCurrentDate())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
@ -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()

View File

@ -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<Int> = _dailyGoal.asStateFlow()
private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = preferencesManager.waterGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
val todayWaterRecords: StateFlow<List<WaterRecord>> =
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)
}
}