Merge branch 'development' into timers
This commit is contained in:
		
						commit
						ce001898fd
					
				
					 17 changed files with 229 additions and 38 deletions
				
			
		|  | @ -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) | ||||
|     } | ||||
| } | ||||
|  | @ -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)) } | ||||
| } | ||||
|  |  | |||
|  | @ -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" | ||||
|                 ) | ||||
|             }} | ||||
|         ) {} | ||||
|     } | ||||
| } | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -29,5 +29,6 @@ interface AccountDAO { | |||
|     suspend fun sendRecoveryEmail(email: String) | ||||
|     suspend fun signUpWithEmailAndPassword(email: String, password: String) | ||||
|     suspend fun deleteAccount() | ||||
| 
 | ||||
|     suspend fun signOut() | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
| } | ||||
|  | @ -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) } | ||||
|     } | ||||
| } | ||||
|  | @ -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" | ||||
| } | ||||
|  | @ -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) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 ( | ||||
|             {}, | ||||
|             {_, _ -> {}} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| package be.ugent.sel.studeez.screens.profile | ||||
| 
 | ||||
| data class ProfileEditUiState ( | ||||
|     val username: String = "" | ||||
| ) | ||||
|  | @ -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) | ||||
|     } | ||||
| } | ||||
|  | @ -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) | ||||
|         ) | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -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) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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( | ||||
|  |  | |||
|  | @ -5,13 +5,21 @@ | |||
|     <string name="email">Email</string> | ||||
|     <string name="password">Password</string> | ||||
|     <string name="repeat_password">Repeat password</string> | ||||
|     <string name="generic_error">Something wrong happened. Please try again.</string> | ||||
|     <string name="email_error">Please insert a valid email.</string> | ||||
|     <string name="cancel">Cancel</string> | ||||
|     <string name="try_again">Try again</string> | ||||
|     <string name="go_back">Go back</string> | ||||
|     <string name="menu">Menu</string> | ||||
| 
 | ||||
|         <!-- Actions --> | ||||
|         <string name="confirm">Confirm</string> | ||||
|         <string name="save">Save</string> | ||||
|         <string name="cancel">Cancel</string> | ||||
|         <string name="go_back">Go back</string> | ||||
|         <string name="next">Next</string> | ||||
| 
 | ||||
|         <!-- Messages --> | ||||
|         <string name="success">Success!</string> | ||||
|         <string name="try_again">Try again</string> | ||||
|         <string name="generic_error">Something wrong happened. Please try again.</string> | ||||
|         <string name="email_error">Please insert a valid email.</string> | ||||
| 
 | ||||
|     <!-- SignUpScreen --> | ||||
|     <string name="create_account">Create account</string> | ||||
|     <string name="password_error">Your password should have at least six characters and include one digit, one lower case letter and one upper case letter.</string> | ||||
|  | @ -39,6 +47,12 @@ | |||
|     <!-- Profile --> | ||||
|     <string name="profile">Profile</string> | ||||
|     <string name="no_username">Unknown username</string> | ||||
|     <string name="edit_profile">Edit profile</string> | ||||
|     <string name="editing_profile">Editing profile</string> | ||||
|     <string name="delete_profile">Delete profile</string> | ||||
| 
 | ||||
|     <!-- Friends --> | ||||
|     <string name="friends">Friends</string> | ||||
| 
 | ||||
|     <!-- Drawer / SideMenu --> | ||||
|     <string name="log_out">Log out</string> | ||||
|  |  | |||
		Reference in a new issue
	
	 lbarraga
						lbarraga