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.ui.theme.StepDrinkTheme
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
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -28,11 +28,13 @@ class MainActivity : ComponentActivity() {
val navController = rememberNavController() val navController = rememberNavController()
val stepViewModel: StepViewModel = viewModel() val stepViewModel: StepViewModel = viewModel()
val waterViewModel: WaterViewModel = viewModel() val waterViewModel: WaterViewModel = viewModel()
val profileViewModel: ProfileViewModel = viewModel()
NavGraph( NavGraph(
navController = navController, navController = navController,
stepViewModel = stepViewModel, 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.home.HomeScreen
import com.example.stepdrink.ui.screen.steps.StepsScreen import com.example.stepdrink.ui.screen.steps.StepsScreen
import com.example.stepdrink.ui.screen.water.WaterScreen 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.StepViewModel
import com.example.stepdrink.viewmodel.WaterViewModel import com.example.stepdrink.viewmodel.WaterViewModel
import com.example.stepdrink.viewmodel.ProfileViewModel
@Composable @Composable
fun NavGraph( fun NavGraph(
navController: NavHostController, navController: NavHostController,
stepViewModel: StepViewModel, stepViewModel: StepViewModel,
waterViewModel: WaterViewModel waterViewModel: WaterViewModel,
profileViewModel: ProfileViewModel
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
@ -24,7 +27,8 @@ fun NavGraph(
HomeScreen( HomeScreen(
navController = navController, navController = navController,
stepViewModel = stepViewModel, stepViewModel = stepViewModel,
waterViewModel = waterViewModel waterViewModel = waterViewModel,
profileViewModel = profileViewModel
) )
} }
@ -41,5 +45,11 @@ fun NavGraph(
viewModel = waterViewModel 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 Home : Screen("home")
object Steps : Screen("steps") object Steps : Screen("steps")
object Water : Screen("water") 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.foundation.layout.*
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.WaterDrop import androidx.compose.material.icons.filled.WaterDrop
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* 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.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
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
navController: NavController, navController: NavController,
stepViewModel: StepViewModel, stepViewModel: StepViewModel,
waterViewModel: WaterViewModel waterViewModel: WaterViewModel,
profileViewModel: ProfileViewModel
) { ) {
val todaySteps by stepViewModel.todaySteps.collectAsState() val todaySteps by stepViewModel.todaySteps.collectAsState()
val stepGoal by stepViewModel.dailyGoal.collectAsState() val stepGoal by stepViewModel.dailyGoal.collectAsState()
val todayWater by waterViewModel.todayTotalWater.collectAsState() val todayWater by waterViewModel.todayTotalWater.collectAsState()
val waterGoal by waterViewModel.dailyGoal.collectAsState() val waterGoal by waterViewModel.dailyGoal.collectAsState()
val userName by profileViewModel.userName.collectAsState()
Scaffold( Scaffold(
topBar = { topBar = {
@ -35,6 +40,11 @@ fun HomeScreen(
text = "Step & Drink", text = "Step & Drink",
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
},
actions = {
IconButton(onClick = { navController.navigate(Screen.Profile.route) }) {
Icon(Icons.Default.Person, "Profil")
}
}, },
colors = TopAppBarDefaults.topAppBarColors( colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer, containerColor = MaterialTheme.colorScheme.primaryContainer,
@ -50,6 +60,11 @@ fun HomeScreen(
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Text(
text = "Halo, $userName! 👋",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
Text( Text(
text = "Aktivitas Hari Ini", text = "Aktivitas Hari Ini",
style = MaterialTheme.typography.headlineSmall, 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.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
@ -18,8 +19,10 @@ class StepViewModel(application: Application) : AndroidViewModel(application) {
) )
val stepCounterManager: StepCounterManager = StepCounterManager(application) val stepCounterManager: StepCounterManager = StepCounterManager(application)
private val _dailyGoal = MutableStateFlow(10000) private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = _dailyGoal.asStateFlow()
val dailyGoal: StateFlow<Int> = preferencesManager.stepGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 10000)
val todaySteps: StateFlow<StepRecord?> = repository.getStepsByDate(DateUtils.getCurrentDate()) val todaySteps: StateFlow<StepRecord?> = repository.getStepsByDate(DateUtils.getCurrentDate())
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) .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() { override fun onCleared() {
super.onCleared() super.onCleared()
stepCounterManager.stopTracking() stepCounterManager.stopTracking()

View File

@ -4,6 +4,7 @@ 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.database.AppDatabase 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.local.entity.WaterRecord
import com.example.stepdrink.data.repository.WaterRepository import com.example.stepdrink.data.repository.WaterRepository
import com.example.stepdrink.util.DateUtils import com.example.stepdrink.util.DateUtils
@ -16,8 +17,10 @@ class WaterViewModel(application: Application) : AndroidViewModel(application) {
AppDatabase.getDatabase(application).waterDao() AppDatabase.getDatabase(application).waterDao()
) )
private val _dailyGoal = MutableStateFlow(2000) // 2000ml = 2 liter private val preferencesManager = PreferencesManager(application)
val dailyGoal: StateFlow<Int> = _dailyGoal.asStateFlow()
val dailyGoal: StateFlow<Int> = preferencesManager.waterGoal
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 2000)
val todayWaterRecords: StateFlow<List<WaterRecord>> = val todayWaterRecords: StateFlow<List<WaterRecord>> =
repository.getWaterRecordsByDate(DateUtils.getCurrentDate()) repository.getWaterRecordsByDate(DateUtils.getCurrentDate())
@ -43,12 +46,4 @@ class WaterViewModel(application: Application) : AndroidViewModel(application) {
repository.deleteWaterRecord(record) repository.deleteWaterRecord(record)
} }
} }
fun setDailyGoal(goal: Int) {
_dailyGoal.value = goal
}
fun getProgressPercentage(): Float {
return (todayTotalWater.value.toFloat() / _dailyGoal.value.toFloat()).coerceIn(0f, 1f)
}
} }