diff --git a/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt b/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt index d112db1..ff46729 100644 --- a/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt +++ b/app/src/main/java/be/ugent/sel/studeez/StudeezApp.kt @@ -21,6 +21,7 @@ import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.screens.home.HomeScreen import be.ugent.sel.studeez.screens.log_in.LoginScreen import be.ugent.sel.studeez.screens.session.SessionScreen +import be.ugent.sel.studeez.screens.profile.EditProfileScreen import be.ugent.sel.studeez.screens.profile.ProfileScreen import be.ugent.sel.studeez.screens.sign_up.SignUpScreen import be.ugent.sel.studeez.screens.splash.SplashScreen @@ -79,12 +80,16 @@ fun resources(): Resources { fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) { - val openAndPopUp: (String, String) -> Unit = { - route, popUp -> appState.navigateAndPopUp(route, popUp) + val goBack: () -> Unit = { + appState.popUp() } val open: (String) -> Unit = { - route -> appState.navigate(route) + route -> appState.navigate(route) + } + + val openAndPopUp: (String, String) -> Unit = { + route, popUp -> appState.navigateAndPopUp(route, popUp) } composable(StudeezDestinations.SPLASH_SCREEN) { @@ -120,4 +125,9 @@ fun NavGraphBuilder.studeezGraph(appState: StudeezAppstate) { // TODO Timers screen // TODO Settings screen + + // Edit screens + composable(StudeezDestinations.EDIT_PROFILE_SCREEN) { + EditProfileScreen(goBack, openAndPopUp) + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt index e174ef2..64c7352 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/ButtonComposable.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.sp @Composable + fun BasicTextButton(@StringRes text: Int, modifier: Modifier, action: () -> Unit) { TextButton(onClick = action, modifier = modifier) { Text(text = stringResource(text)) } } diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt index 5983d37..009281f 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/PrimaryScreenComposable.kt @@ -1,8 +1,10 @@ package be.ugent.sel.studeez.common.composable import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope import androidx.compose.material.* import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Menu import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -20,6 +22,7 @@ fun PrimaryScreenTemplate( title: String, open: (String) -> Unit, openAndPopUp: (String, String) -> Unit, + action: @Composable RowScope.() -> Unit, content: @Composable (PaddingValues) -> Unit ) { val scaffoldState: ScaffoldState = rememberScaffoldState() @@ -39,7 +42,8 @@ fun PrimaryScreenTemplate( contentDescription = resources().getString(R.string.menu) ) } - } + }, + actions = action ) }, drawerContent = { @@ -62,7 +66,13 @@ fun PrimaryScreenPreview() { PrimaryScreenTemplate( "Preview screen", { _ -> {}}, - { _, _ -> {}} + { _, _ -> {}}, + { IconButton(onClick = { /*TODO*/ }) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit" + ) + }} ) {} } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt index 5766607..2c0b450 100644 --- a/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt +++ b/app/src/main/java/be/ugent/sel/studeez/common/composable/TextFieldComposable.kt @@ -11,14 +11,15 @@ import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Person import androidx.compose.runtime.* -import be.ugent.sel.studeez.R.string as AppText -import be.ugent.sel.studeez.R.drawable as AppIcon import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import be.ugent.sel.studeez.common.ext.fieldModifier +import be.ugent.sel.studeez.R.drawable as AppIcon +import be.ugent.sel.studeez.R.string as AppText @Composable fun BasicField( @@ -36,6 +37,20 @@ fun BasicField( ) } +@Composable +fun LabelledInputField( + value: String, + onNewValue: (String) -> Unit, + @StringRes label: Int +) { + OutlinedTextField( + value = value, + onValueChange = onNewValue, + label = { Text(text = stringResource(id = label)) }, + modifier = Modifier.fieldModifier() + ) +} + @Composable fun UsernameField( value: String, diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt index 96ecb74..c813ec6 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/AccountDAO.kt @@ -29,5 +29,6 @@ interface AccountDAO { suspend fun sendRecoveryEmail(email: String) suspend fun signUpWithEmailAndPassword(email: String, password: String) suspend fun deleteAccount() + suspend fun signOut() } diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt index 8b6f357..b96cf17 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/UserDAO.kt @@ -4,4 +4,10 @@ interface UserDAO { suspend fun getUsername(): String? suspend fun save(newUsername: String) + + /** + * Delete all references to this user in the database. Similar to the deleteCascade in + * relational databases. + */ + suspend fun deleteUserReferences() } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt index 8ac779b..3158b88 100644 --- a/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt +++ b/app/src/main/java/be/ugent/sel/studeez/domain/implementation/FirebaseUserDAO.kt @@ -1,19 +1,13 @@ package be.ugent.sel.studeez.domain.implementation -import androidx.compose.runtime.rememberCoroutineScope +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.snackbar.SnackbarManager import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.UserDAO import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.tasks.await import javax.inject.Inject -import kotlin.coroutines.coroutineContext class FirebaseUserDAO @Inject constructor( private val firestore: FirebaseFirestore, @@ -34,4 +28,10 @@ class FirebaseUserDAO @Inject constructor( companion object { private const val USER_COLLECTION = "users" } + + override suspend fun deleteUserReferences() { + currentUserDocument().delete() + .addOnSuccessListener { SnackbarManager.showMessage(R.string.success) } + .addOnFailureListener { SnackbarManager.showMessage(R.string.generic_error) } + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt index 51cb871..144cd45 100644 --- a/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt +++ b/app/src/main/java/be/ugent/sel/studeez/navigation/StudeezDestinations.kt @@ -13,4 +13,7 @@ object StudeezDestinations { // const val TIMERS_SCREEN = "timers" // const val SETTINGS_SCREEN = "settings" + + // Edit screens + const val EDIT_PROFILE_SCREEN = "edit_profile" } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt index e8c88c1..7ca2559 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/home/HomeScreen.kt @@ -1,5 +1,9 @@ package be.ugent.sel.studeez.screens.home +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel @@ -18,10 +22,21 @@ fun HomeScreen( PrimaryScreenTemplate( title = resources().getString(R.string.home), open = open, - openAndPopUp = openAndPopUp + openAndPopUp = openAndPopUp, + action = { FriendsAction() } ) { BasicButton(R.string.start_session, Modifier.basicButton()) { viewModel.onStartSessionClick(open) } } +} + +@Composable +fun FriendsAction () { + IconButton(onClick = { /*TODO*/ }) { + Icon( + imageVector = Icons.Default.Person, + contentDescription = resources().getString(R.string.friends) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/navbar/NavigationBarViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/navbar/NavigationBarViewModel.kt index 1814d84..75613d5 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/navbar/NavigationBarViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/navbar/NavigationBarViewModel.kt @@ -2,7 +2,6 @@ package be.ugent.sel.studeez.screens.navbar import be.ugent.sel.studeez.domain.AccountDAO import be.ugent.sel.studeez.domain.LogService -import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.navigation.StudeezDestinations.HOME_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.PROFILE_SCREEN import be.ugent.sel.studeez.screens.StudeezViewModel diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt new file mode 100644 index 0000000..eb12ac7 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditScreen.kt @@ -0,0 +1,55 @@ +package be.ugent.sel.studeez.screens.profile + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.composable.BasicTextButton +import be.ugent.sel.studeez.common.composable.LabelledInputField +import be.ugent.sel.studeez.common.composable.SecondaryScreenTemplate +import be.ugent.sel.studeez.common.ext.textButton +import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.ui.theme.StudeezTheme + +@Composable +fun EditProfileScreen( + goBack: () -> Unit, + openAndPopUp: (String, String) -> Unit, + viewModel: ProfileEditViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState + + SecondaryScreenTemplate( + title = resources().getString(R.string.editing_profile), + popUp = goBack + ) { + Column { + LabelledInputField( + value = uiState.username, + onNewValue = viewModel::onUsernameChange, + label = R.string.username + ) + + BasicTextButton(text = R.string.save, Modifier.textButton()) { + viewModel.onSaveClick() + } + BasicTextButton(text = R.string.delete_profile, Modifier.textButton()) { + viewModel.onDeleteClick(openAndPopUp) + } + } + } +} + +@Preview +@Composable +fun EditProfileScreenComposable() { + StudeezTheme { + EditProfileScreen ( + {}, + {_, _ -> {}} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt new file mode 100644 index 0000000..9ecaba3 --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditUiState.kt @@ -0,0 +1,5 @@ +package be.ugent.sel.studeez.screens.profile + +data class ProfileEditUiState ( + val username: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt new file mode 100644 index 0000000..cb270be --- /dev/null +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileEditViewModel.kt @@ -0,0 +1,48 @@ +package be.ugent.sel.studeez.screens.profile + +import androidx.compose.runtime.mutableStateOf +import be.ugent.sel.studeez.R +import be.ugent.sel.studeez.common.snackbar.SnackbarManager +import be.ugent.sel.studeez.domain.AccountDAO +import be.ugent.sel.studeez.domain.LogService +import be.ugent.sel.studeez.domain.UserDAO +import be.ugent.sel.studeez.navigation.StudeezDestinations +import be.ugent.sel.studeez.screens.StudeezViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class ProfileEditViewModel @Inject constructor( + private val accountDAO: AccountDAO, + private val userDAO: UserDAO, + logService: LogService +) : StudeezViewModel(logService) { + + var uiState = mutableStateOf(ProfileEditUiState()) + private set + + init { + launchCatching { + uiState.value = uiState.value.copy(username = userDAO.getUsername()!!) + } + } + + fun onUsernameChange(newValue: String) { + uiState.value = uiState.value.copy(username = newValue) + } + + fun onSaveClick() { + launchCatching { + userDAO.save(uiState.value.username) + SnackbarManager.showMessage(R.string.success) + } + } + + fun onDeleteClick(openAndPopUp: (String, String) -> Unit) { + launchCatching { + userDAO.deleteUserReferences() // Delete references + accountDAO.deleteAccount() // Delete authentication + } + openAndPopUp(StudeezDestinations.SIGN_UP_SCREEN, StudeezDestinations.EDIT_PROFILE_SCREEN) + } +} \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt index 56568c4..6ec4a01 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileScreen.kt @@ -1,5 +1,9 @@ package be.ugent.sel.studeez.screens.profile +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.runtime.* import androidx.hilt.navigation.compose.hiltViewModel import be.ugent.sel.studeez.R @@ -22,8 +26,22 @@ fun ProfileScreen( PrimaryScreenTemplate( title = resources().getString(AppText.profile), open = open, - openAndPopUp = openAndPopUp + openAndPopUp = openAndPopUp, + action = { EditAction { viewModel.onEditProfileClick(open) } } ) { Headline(text = (username ?: resources().getString(R.string.no_username))) } +} + +@Composable +fun EditAction( + onClick: () -> Unit +) { + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = resources().getString(AppText.edit_profile) + ) + + } } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt index 1f6b1a2..e24defd 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/profile/ProfileViewModel.kt @@ -1,20 +1,10 @@ package be.ugent.sel.studeez.screens.profile -import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.viewModelScope -import be.ugent.sel.studeez.R import be.ugent.sel.studeez.domain.LogService import be.ugent.sel.studeez.domain.UserDAO -import be.ugent.sel.studeez.resources +import be.ugent.sel.studeez.navigation.StudeezDestinations import be.ugent.sel.studeez.screens.StudeezViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - import javax.inject.Inject @HiltViewModel @@ -27,4 +17,8 @@ class ProfileViewModel @Inject constructor( return userDAO.getUsername() } + fun onEditProfileClick(open: (String) -> Unit) { + open(StudeezDestinations.EDIT_PROFILE_SCREEN) + } + } \ No newline at end of file diff --git a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt index dacb7db..91dde13 100644 --- a/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt +++ b/app/src/main/java/be/ugent/sel/studeez/screens/sign_up/SignUpViewModel.kt @@ -13,11 +13,8 @@ import be.ugent.sel.studeez.navigation.StudeezDestinations.LOGIN_SCREEN import be.ugent.sel.studeez.navigation.StudeezDestinations.SIGN_UP_SCREEN import be.ugent.sel.studeez.screens.StudeezViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.take -import be.ugent.sel.studeez.R.string as AppText import javax.inject.Inject +import be.ugent.sel.studeez.R.string as AppText @HiltViewModel class SignUpViewModel @Inject constructor( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b17c518..b48d7f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,13 +5,21 @@ Email Password Repeat password - Something wrong happened. Please try again. - Please insert a valid email. - Cancel - Try again - Go back Menu + + Confirm + Save + Cancel + Go back + Next + + + Success! + Try again + Something wrong happened. Please try again. + Please insert a valid email. + Create account Your password should have at least six characters and include one digit, one lower case letter and one upper case letter. @@ -39,6 +47,12 @@ Profile Unknown username + Edit profile + Editing profile + Delete profile + + + Friends Log out