From 19e690f3eb2f1248a434ad289dd88ad3bc53ae49 Mon Sep 17 00:00:00 2001 From: Stefano Cappa Date: Mon, 13 Jan 2025 00:21:25 +0100 Subject: [PATCH] feat: complete profile screen with regenApiToken + logout Signed-off-by: Stefano Cappa --- .../main/java/eu/homeanthill/MainActivity.kt | 3 + .../api/requests/ProfileServices.kt | 6 +- .../java/eu/homeanthill/di/KoinModules.kt | 2 +- .../homeanthill/repository/LoginRepository.kt | 11 ++ .../repository/ProfileRepository.kt | 2 +- .../homeanthill/ui/components/CircleImage.kt | 30 +++++ .../eu/homeanthill/ui/components/TopAppBar.kt | 31 +---- .../ui/screens/profile/ProfileScreen.kt | 127 ++++++++++++++---- .../ui/screens/profile/ProfileViewModel.kt | 35 +++-- app/src/main/res/values/strings.xml | 3 + 10 files changed, 174 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/eu/homeanthill/ui/components/CircleImage.kt diff --git a/app/src/main/java/eu/homeanthill/MainActivity.kt b/app/src/main/java/eu/homeanthill/MainActivity.kt index e484be8..6cf2495 100644 --- a/app/src/main/java/eu/homeanthill/MainActivity.kt +++ b/app/src/main/java/eu/homeanthill/MainActivity.kt @@ -97,8 +97,11 @@ class MainActivity : ComponentActivity() { ) { val profileViewModel = koinViewModel() val profileUiState by profileViewModel.profileUiState.collectAsStateWithLifecycle() + val apiTokenUiState by profileViewModel.apiTokenUiState.collectAsStateWithLifecycle() ProfileScreen( profileUiState = profileUiState, + apiTokenUiState = apiTokenUiState, + profileViewModel = profileViewModel, navController = navController ) } diff --git a/app/src/main/java/eu/homeanthill/api/requests/ProfileServices.kt b/app/src/main/java/eu/homeanthill/api/requests/ProfileServices.kt index 6b2c085..1e2427f 100644 --- a/app/src/main/java/eu/homeanthill/api/requests/ProfileServices.kt +++ b/app/src/main/java/eu/homeanthill/api/requests/ProfileServices.kt @@ -15,6 +15,8 @@ interface ProfileServices { suspend fun getProfile(): Response @Headers("Accept: application/json") - @POST("profile/{id}/tokens") - suspend fun postRegenApiToken(@Path("id") id: String): Response + @POST("profiles/{id}/tokens") + suspend fun postRegenApiToken( + @Path("id") id: String + ): Response } \ No newline at end of file diff --git a/app/src/main/java/eu/homeanthill/di/KoinModules.kt b/app/src/main/java/eu/homeanthill/di/KoinModules.kt index 5ce5a0e..73a426d 100644 --- a/app/src/main/java/eu/homeanthill/di/KoinModules.kt +++ b/app/src/main/java/eu/homeanthill/di/KoinModules.kt @@ -36,7 +36,7 @@ val viewModelModule = module { fcmTokenRepository = get() ) } - viewModel { ProfileViewModel(profileRepository = get()) } + viewModel { ProfileViewModel(loginRepository = get(), profileRepository = get()) } } val repositoryModule = module { diff --git a/app/src/main/java/eu/homeanthill/repository/LoginRepository.kt b/app/src/main/java/eu/homeanthill/repository/LoginRepository.kt index 5e217de..cf73d67 100644 --- a/app/src/main/java/eu/homeanthill/repository/LoginRepository.kt +++ b/app/src/main/java/eu/homeanthill/repository/LoginRepository.kt @@ -53,4 +53,15 @@ class LoginRepository(private val context: Context) { .putString("profile", json) .apply() } + + fun logout() { + context + .getSharedPreferences("home-anthill", Context.MODE_PRIVATE) + .edit() + .remove("profile") + .remove("fcmToken") + .remove("jwt") + .remove("sessionCookie") + .apply() + } } \ No newline at end of file diff --git a/app/src/main/java/eu/homeanthill/repository/ProfileRepository.kt b/app/src/main/java/eu/homeanthill/repository/ProfileRepository.kt index 4e39716..dae690a 100644 --- a/app/src/main/java/eu/homeanthill/repository/ProfileRepository.kt +++ b/app/src/main/java/eu/homeanthill/repository/ProfileRepository.kt @@ -1,8 +1,8 @@ package eu.homeanthill.repository -import eu.homeanthill.api.model.ProfileAPITokenResponse import java.io.IOException +import eu.homeanthill.api.model.ProfileAPITokenResponse import eu.homeanthill.api.model.Profile import eu.homeanthill.api.requests.ProfileServices diff --git a/app/src/main/java/eu/homeanthill/ui/components/CircleImage.kt b/app/src/main/java/eu/homeanthill/ui/components/CircleImage.kt new file mode 100644 index 0000000..2e6571e --- /dev/null +++ b/app/src/main/java/eu/homeanthill/ui/components/CircleImage.kt @@ -0,0 +1,30 @@ +package eu.homeanthill.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.Dp +import coil3.compose.AsyncImage + +@Composable +fun CircleImage(imageUrl: String, size: Dp) { + Box( + modifier = Modifier + .size(size) + .clip(CircleShape) + ) { + AsyncImage( + model = imageUrl, + contentDescription = "image", + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxSize() + .clip(CircleShape) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/homeanthill/ui/components/TopAppBar.kt b/app/src/main/java/eu/homeanthill/ui/components/TopAppBar.kt index 8c9f83c..0c7e194 100644 --- a/app/src/main/java/eu/homeanthill/ui/components/TopAppBar.kt +++ b/app/src/main/java/eu/homeanthill/ui/components/TopAppBar.kt @@ -1,11 +1,5 @@ package eu.homeanthill.ui.components -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.CenterAlignedTopAppBar @@ -16,16 +10,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import coil3.compose.AsyncImage import eu.homeanthill.R import eu.homeanthill.api.model.Profile @@ -64,30 +53,12 @@ fun TopAppBar( IconButton(onClick = { navController?.navigate(route = MainRoute.Profile.name) }) { - CircleImageWithBorder(profile.github.avatarURL) + CircleImage(profile.github.avatarURL, 100.dp) } } }) } -@Composable -fun CircleImageWithBorder(imageUrl: String) { - Box( - modifier = Modifier - .size(100.dp) - .clip(CircleShape) - ) { - AsyncImage( - model = imageUrl, - contentDescription = "Circular Image with Border", - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .clip(CircleShape) - ) - } -} - @Composable @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) fun TopAppBarPreview() { diff --git a/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileScreen.kt index 72eea7c..fbf5430 100644 --- a/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileScreen.kt @@ -1,65 +1,134 @@ package eu.homeanthill.ui.screens.profile +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.navigation.NavController import eu.homeanthill.R +import eu.homeanthill.ui.components.CircleImage import eu.homeanthill.ui.components.TopAppBar +import eu.homeanthill.ui.navigation.MainRoute +import kotlinx.coroutines.launch @Composable fun ProfileScreen( profileUiState: ProfileViewModel.ProfileUiState, + apiTokenUiState: ProfileViewModel.ApiTokenUiState, + profileViewModel: ProfileViewModel, navController: NavController, ) { - Scaffold( - topBar = { - TopAppBar( - appbarTitle = stringResource(R.string.profile), - onBackPressed = { navController.popBackStack() } - ) - }, - content = { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(horizontal = 8.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - when (profileUiState) { - is ProfileViewModel.ProfileUiState.Error -> { + val coroutineScope = rememberCoroutineScope() + + Scaffold(topBar = { + TopAppBar(appbarTitle = stringResource(R.string.profile), + onBackPressed = { navController.popBackStack() }) + }, content = { padding -> + Column( + modifier = Modifier + .padding(padding) + .fillMaxSize() + .padding(horizontal = 8.dp, vertical = 25.dp), + verticalArrangement = Arrangement.Top, + horizontalAlignment = Alignment.CenterHorizontally + ) { + when (profileUiState) { + is ProfileViewModel.ProfileUiState.Error -> { + Text( + text = profileUiState.errorMessage, + color = MaterialTheme.colorScheme.error, + ) + } + is ProfileViewModel.ProfileUiState.Loading -> { + CircularProgressIndicator() + } + is ProfileViewModel.ProfileUiState.Idle -> { + profileUiState.profile?.github?.avatarURL?.let { CircleImage(it, 150.dp) } + Spacer(modifier = Modifier.height(30.dp)) + profileUiState.profile?.github?.login?.let { Text( - text = profileUiState.errorMessage, - color = MaterialTheme.colorScheme.error, + text = it, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, ) } - - is ProfileViewModel.ProfileUiState.Loading -> { - CircularProgressIndicator() + profileUiState.profile?.github?.name?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) } - - is ProfileViewModel.ProfileUiState.Idle -> { + profileUiState.profile?.github?.email?.let { Text( - text = profileUiState.profile.toString(), - fontSize = 24.sp + text = it, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, ) } + HorizontalDivider( + thickness = 2.dp, modifier = Modifier.padding(vertical = 20.dp) + ) + when (apiTokenUiState) { + is ProfileViewModel.ApiTokenUiState.Error -> { + Text( + text = "********-****-****-****-************", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + is ProfileViewModel.ApiTokenUiState.Loading -> { + CircularProgressIndicator() + } + is ProfileViewModel.ApiTokenUiState.Idle -> { + Text( + text = apiTokenUiState.apiToken, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + } + } + Button( + onClick = { + coroutineScope.launch { + Log.d("ProfileScreen", "id = $profileUiState.profile?.id") + profileViewModel.regenApiToken(profileUiState.profile?.id) + } + }, enabled = true, modifier = Modifier.padding(top = 10.dp, bottom = 10.dp) + ) { + Text(text = stringResource(R.string.profile_regen_apitoken)) + } + HorizontalDivider( + thickness = 2.dp, modifier = Modifier.padding(vertical = 20.dp) + ) + Button( + onClick = { + profileViewModel.logout() + navController.navigate(route = MainRoute.Login.name) + }, + enabled = true, + ) { + Text(text = stringResource(R.string.profile_logout)) + } } } } - ) + }) } \ No newline at end of file diff --git a/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileViewModel.kt b/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileViewModel.kt index 141aac4..ebc72db 100644 --- a/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileViewModel.kt @@ -11,9 +11,11 @@ import java.io.IOException import eu.homeanthill.api.model.ProfileAPITokenResponse import eu.homeanthill.api.model.Profile +import eu.homeanthill.repository.LoginRepository import eu.homeanthill.repository.ProfileRepository class ProfileViewModel( + private val loginRepository: LoginRepository, private val profileRepository: ProfileRepository, ) : ViewModel() { companion object { @@ -34,13 +36,32 @@ class ProfileViewModel( private val _profileUiState = MutableStateFlow(ProfileUiState.Idle(null)) val profileUiState: StateFlow = _profileUiState - private val _apiTokenUiState = MutableStateFlow(ApiTokenUiState.Idle("")) + private val _apiTokenUiState = + MutableStateFlow(ApiTokenUiState.Idle("********-****-****-****-************")) val apiTokenUiState: StateFlow = _apiTokenUiState init { init() } + suspend fun regenApiToken(id: String?) { + if (id == null) { + return + } + _apiTokenUiState.emit(ApiTokenUiState.Loading) + delay(250) + try { + val response: ProfileAPITokenResponse = profileRepository.repoPostRegenAPIToken(id) + _apiTokenUiState.emit(ApiTokenUiState.Idle(response.apiToken)) + } catch (err: IOException) { + _apiTokenUiState.emit(ApiTokenUiState.Error(err.message.toString())) + } + } + + fun logout() { + loginRepository.logout() + } + private fun init() { viewModelScope.launch { _profileUiState.emit(ProfileUiState.Loading) @@ -55,16 +76,4 @@ class ProfileViewModel( return@launch } } - - private suspend fun regenApiToken(id: String) { - _apiTokenUiState.emit(ApiTokenUiState.Loading) - delay(250) - try { - val response: ProfileAPITokenResponse = profileRepository.repoPostRegenAPIToken(id) - Log.d(TAG, "regenApiToken - response = $response") - _apiTokenUiState.emit(ApiTokenUiState.Idle(response.apiToken)) - } catch (err: IOException) { - _apiTokenUiState.emit(ApiTokenUiState.Error(err.message.toString())) - } - } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae1f384..f2557c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,6 +15,9 @@ The notification permission is required for some functionality. Profile + Regenerate API Token + Logout + Homes Devices Back