Skip to content

Commit

Permalink
feat: complete profile screen with regenApiToken + logout
Browse files Browse the repository at this point in the history
Signed-off-by: Stefano Cappa <[email protected]>
  • Loading branch information
Ks89 committed Jan 12, 2025
1 parent ec81071 commit 19e690f
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 76 deletions.
3 changes: 3 additions & 0 deletions app/src/main/java/eu/homeanthill/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,11 @@ class MainActivity : ComponentActivity() {
) {
val profileViewModel = koinViewModel<ProfileViewModel>()
val profileUiState by profileViewModel.profileUiState.collectAsStateWithLifecycle()
val apiTokenUiState by profileViewModel.apiTokenUiState.collectAsStateWithLifecycle()
ProfileScreen(
profileUiState = profileUiState,
apiTokenUiState = apiTokenUiState,
profileViewModel = profileViewModel,
navController = navController
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ interface ProfileServices {
suspend fun getProfile(): Response<Profile>

@Headers("Accept: application/json")
@POST("profile/{id}/tokens")
suspend fun postRegenApiToken(@Path("id") id: String): Response<ProfileAPITokenResponse>
@POST("profiles/{id}/tokens")
suspend fun postRegenApiToken(
@Path("id") id: String
): Response<ProfileAPITokenResponse>
}
2 changes: 1 addition & 1 deletion app/src/main/java/eu/homeanthill/di/KoinModules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ val viewModelModule = module {
fcmTokenRepository = get()
)
}
viewModel { ProfileViewModel(profileRepository = get()) }
viewModel { ProfileViewModel(loginRepository = get(), profileRepository = get()) }
}

val repositoryModule = module {
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/eu/homeanthill/repository/LoginRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand Down
30 changes: 30 additions & 0 deletions app/src/main/java/eu/homeanthill/ui/components/CircleImage.kt
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
31 changes: 1 addition & 30 deletions app/src/main/java/eu/homeanthill/ui/components/TopAppBar.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
127 changes: 98 additions & 29 deletions app/src/main/java/eu/homeanthill/ui/screens/profile/ProfileScreen.kt
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
)
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,13 +36,32 @@ class ProfileViewModel(

private val _profileUiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Idle(null))
val profileUiState: StateFlow<ProfileUiState> = _profileUiState
private val _apiTokenUiState = MutableStateFlow<ApiTokenUiState>(ApiTokenUiState.Idle(""))
private val _apiTokenUiState =
MutableStateFlow<ApiTokenUiState>(ApiTokenUiState.Idle("********-****-****-****-************"))
val apiTokenUiState: StateFlow<ApiTokenUiState> = _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)
Expand All @@ -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()))
}
}
}
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<string name="home_permission_required">The notification permission is required for some functionality.</string>

<string name="profile">Profile</string>
<string name="profile_regen_apitoken">Regenerate API Token</string>
<string name="profile_logout">Logout</string>

<string name="homes">Homes</string>
<string name="devices">Devices</string>
<string name="back">Back</string>
Expand Down

0 comments on commit 19e690f

Please sign in to comment.