Skip to content

Commit ff09348

Browse files
committed
feat: profile screen and refactoring
1 parent 47ae31b commit ff09348

File tree

9 files changed

+227
-34
lines changed

9 files changed

+227
-34
lines changed

feature/favourite/src/main/kotlin/com/espressodev/gptmap/feature/favourite/FavouriteScreen.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ import com.espressodev.gptmap.core.designsystem.GmIcons
3535
import com.espressodev.gptmap.core.designsystem.IconType
3636
import com.espressodev.gptmap.core.designsystem.TextType
3737
import com.espressodev.gptmap.core.designsystem.component.GmTopAppBar
38+
import com.espressodev.gptmap.core.designsystem.component.LottieAnimationView
3839
import com.espressodev.gptmap.core.designsystem.theme.GptmapTheme
3940
import com.espressodev.gptmap.core.model.Favourite
4041
import com.espressodev.gptmap.core.model.Response
4142
import java.time.LocalDateTime
4243
import com.espressodev.gptmap.core.designsystem.R.string as AppText
44+
import com.espressodev.gptmap.core.designsystem.R.raw as AppRaw
4345

4446
@Composable
4547
fun FavouriteRoute(
@@ -67,7 +69,7 @@ fun FavouriteScreen(
6769
Scaffold(
6870
topBar = {
6971
GmTopAppBar(
70-
textType = TextType.Res(AppText.favourite),
72+
text = TextType.Res(AppText.favourite),
7173
icon = IconType.Vector(GmIcons.FavouriteFilled),
7274
onBackClick = popUp
7375
)
@@ -95,7 +97,7 @@ fun FavouriteScreen(
9597
}
9698

9799
is Response.Failure -> {
98-
// LoadingAnimation(animId = AppRaw.confused_man_404)
100+
LottieAnimationView(AppRaw.confused_man_404)
99101
}
100102

101103
Response.Loading -> {}

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapNavigation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fun NavGraphBuilder.mapScreen(
2020
navigateToFavourite: () -> Unit,
2121
navigateToScreenshot: () -> Unit,
2222
navigateToScreenshotGallery: () -> Unit,
23-
navigateToAccount: () -> Unit
23+
navigateToProfile: () -> Unit
2424
) {
2525
composable(
2626
route = "$MAP_ROUTE/{$FAVOURITE_ID}",
@@ -33,7 +33,7 @@ fun NavGraphBuilder.mapScreen(
3333
favouriteId = favouriteId,
3434
navigateToScreenshot = navigateToScreenshot,
3535
navigateToScreenshotGallery = navigateToScreenshotGallery,
36-
navigateToAccount = navigateToAccount
36+
navigateToProfile = navigateToProfile
3737
)
3838
}
3939
}

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapScreen.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.espressodev.gptmap.feature.map
22

33
import android.annotation.SuppressLint
4-
import android.util.Log
54
import androidx.compose.animation.AnimatedVisibility
65
import androidx.compose.animation.core.LinearEasing
76
import androidx.compose.animation.core.animateFloatAsState
@@ -107,7 +106,7 @@ fun MapRoute(
107106
navigateToStreetView: (Float, Float) -> Unit,
108107
navigateToFavourite: () -> Unit,
109108
navigateToScreenshot: () -> Unit,
110-
navigateToAccount: () -> Unit,
109+
navigateToProfile: () -> Unit,
111110
navigateToScreenshotGallery: () -> Unit,
112111
favouriteId: String,
113112
modifier: Modifier = Modifier,
@@ -132,7 +131,7 @@ fun MapRoute(
132131
uiState = uiState,
133132
onFavouriteClick = navigateToFavourite,
134133
onScreenshotGalleryClick = navigateToScreenshotGallery,
135-
onAccountClick = navigateToAccount,
134+
onProfileClick = navigateToProfile,
136135
onEvent = { event ->
137136
viewModel.onEvent(
138137
event = event,
@@ -163,7 +162,7 @@ private fun MapScreen(
163162
uiState: MapUiState,
164163
onFavouriteClick: () -> Unit,
165164
onScreenshotGalleryClick: () -> Unit,
166-
onAccountClick: () -> Unit,
165+
onProfileClick: () -> Unit,
167166
onEvent: (MapUiEvent) -> Unit,
168167
modifier: Modifier = Modifier,
169168
) {
@@ -185,7 +184,7 @@ private fun MapScreen(
185184
isVisible = uiState.isTopButtonsVisible,
186185
onFavouriteClick = onFavouriteClick,
187186
onScreenshotGalleryClick = onScreenshotGalleryClick,
188-
onAccountClick = onAccountClick,
187+
onAccountClick = onProfileClick,
189188
modifier = modifier
190189
)
191190
Box(modifier = modifier.fillMaxSize()) {
@@ -394,7 +393,6 @@ private fun MapSection(isPinVisible: Boolean, cameraPositionState: CameraPositio
394393
val context = LocalContext.current
395394
val isSystemInDarkTheme = isSystemInDarkTheme()
396395
var isMapLoaded by remember { mutableStateOf(value = false) }
397-
Log.d("MapSection", "isMapLoaded: $isMapLoaded")
398396
val mapProperties = remember {
399397
if (isSystemInDarkTheme) {
400398
MapProperties(

feature/profile/src/main/kotlin/com/espressodev/gptmap/feature/profile/ProfileNavigation.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ fun NavController.navigateToProfile(navOptions: NavOptions? = null) {
1111
navigate(PROFILE_ROUTE, navOptions)
1212
}
1313

14-
fun NavGraphBuilder.profileScreen() {
14+
fun NavGraphBuilder.profileScreen(popUp: () -> Unit, navigateToLogin: () -> Unit) {
1515
composable(PROFILE_ROUTE) {
16-
ProfileRoute()
16+
ProfileRoute(popUp = popUp, navigateToLogin = navigateToLogin)
1717
}
1818
}
Lines changed: 148 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,167 @@
11
package com.espressodev.gptmap.feature.profile
22

3+
import androidx.annotation.StringRes
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Column
37
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.Spacer
9+
import androidx.compose.foundation.layout.fillMaxSize
10+
import androidx.compose.foundation.layout.fillMaxWidth
11+
import androidx.compose.foundation.layout.height
412
import androidx.compose.foundation.layout.padding
5-
import androidx.compose.foundation.lazy.LazyColumn
6-
import androidx.compose.foundation.lazy.LazyRow
7-
import androidx.compose.foundation.lazy.items
8-
import androidx.compose.material.icons.Icons
9-
import androidx.compose.material.icons.filled.ArrowDropDown
10-
import androidx.compose.material3.DropdownMenu
11-
import androidx.compose.material3.DropdownMenuItem
13+
import androidx.compose.foundation.layout.size
14+
import androidx.compose.foundation.layout.width
15+
import androidx.compose.foundation.shape.RoundedCornerShape
16+
import androidx.compose.material3.ExperimentalMaterial3Api
17+
import androidx.compose.material3.FilledIconButton
18+
import androidx.compose.material3.FilledTonalIconButton
1219
import androidx.compose.material3.Icon
1320
import androidx.compose.material3.IconButton
14-
import androidx.compose.material3.OutlinedTextField
21+
import androidx.compose.material3.IconButtonDefaults
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.Scaffold
1524
import androidx.compose.material3.Text
1625
import androidx.compose.runtime.Composable
1726
import androidx.compose.runtime.getValue
18-
import androidx.compose.runtime.mutableStateOf
19-
import androidx.compose.runtime.remember
20-
import androidx.compose.runtime.setValue
27+
import androidx.compose.ui.Alignment
2128
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.draw.clip
30+
import androidx.compose.ui.graphics.vector.ImageVector
31+
import androidx.compose.ui.res.stringResource
32+
import androidx.compose.ui.text.style.TextOverflow
2233
import androidx.compose.ui.tooling.preview.Preview
2334
import androidx.compose.ui.unit.dp
35+
import androidx.hilt.navigation.compose.hiltViewModel
36+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
37+
import com.espressodev.gptmap.core.designsystem.GmIcons
38+
import com.espressodev.gptmap.core.designsystem.IconType
39+
import com.espressodev.gptmap.core.designsystem.TextType
40+
import com.espressodev.gptmap.core.designsystem.component.GmTonalIconButton
41+
import com.espressodev.gptmap.core.designsystem.component.GmTopAppBar
42+
import com.espressodev.gptmap.core.designsystem.component.LetterInCircle
43+
import com.espressodev.gptmap.core.designsystem.component.LottieAnimationView
44+
import com.espressodev.gptmap.core.designsystem.theme.GptmapTheme
45+
import com.espressodev.gptmap.core.model.Response
46+
import com.espressodev.gptmap.core.model.User
47+
import com.espressodev.gptmap.core.designsystem.R.string as AppText
48+
import com.espressodev.gptmap.core.designsystem.R.raw as AppRaw
2449

25-
50+
@OptIn(ExperimentalMaterial3Api::class)
2651
@Composable
27-
fun ProfileRoute() {
52+
fun ProfileRoute(
53+
popUp: () -> Unit,
54+
navigateToLogin: () -> Unit,
55+
viewModel: ProfileViewModel = hiltViewModel()
56+
) {
57+
val user by viewModel.user.collectAsStateWithLifecycle()
58+
Scaffold(
59+
topBar = {
60+
GmTopAppBar(
61+
text = TextType.Res(AppText.profile_top_bar_header),
62+
icon = IconType.Vector(GmIcons.AccountCircleOutlined),
63+
onBackClick = popUp
64+
)
65+
}
66+
) {
67+
when (val result = user) {
68+
is Response.Failure -> LottieAnimationView(AppRaw.confused_man_404)
69+
Response.Loading -> {}
70+
is Response.Success -> ProfileScreen(
71+
user = result.data,
72+
onEditFullNameClick = {},
73+
onInfoClick = {},
74+
onLogOutClick = { viewModel.onLogoutClick(navigateToLogin) },
75+
modifier = Modifier.padding(it)
76+
)
77+
}
78+
}
79+
}
2880

81+
@Composable
82+
fun ProfileScreen(
83+
user: User,
84+
onEditFullNameClick: () -> Unit,
85+
onInfoClick: () -> Unit,
86+
onLogOutClick: () -> Unit,
87+
modifier: Modifier = Modifier
88+
) {
89+
Column(
90+
modifier = modifier
91+
.fillMaxSize()
92+
.padding(vertical = 16.dp, horizontal = 8.dp),
93+
verticalArrangement = Arrangement.spacedBy(8.dp)
94+
) {
95+
Column(
96+
modifier = Modifier.fillMaxWidth(),
97+
horizontalAlignment = Alignment.CenterHorizontally,
98+
verticalArrangement = Arrangement.spacedBy(8.dp)
99+
) {
100+
LetterInCircle(letter = user.fullName.first(), modifier = Modifier.size(120.dp))
101+
Row(
102+
verticalAlignment = Alignment.CenterVertically
103+
) {
104+
Spacer(modifier = Modifier.width(40.dp))
105+
Text(
106+
text = user.fullName,
107+
style = MaterialTheme.typography.headlineLarge,
108+
maxLines = 1,
109+
overflow = TextOverflow.Ellipsis
110+
)
111+
IconButton(onClick = onEditFullNameClick) {
112+
Icon(
113+
imageVector = GmIcons.EditDefault,
114+
contentDescription = stringResource(id = AppText.edit)
115+
)
116+
}
117+
}
118+
}
119+
Spacer(modifier = Modifier.height(8.dp))
120+
ProfileItem(GmIcons.InfoOutlined, AppText.info, onInfoClick)
121+
ProfileItem(GmIcons.LogoutOutlined, AppText.logout, onLogOutClick)
122+
}
29123
}
30124

31125
@Composable
32-
fun ProfileScreen() {
126+
fun ProfileItem(icon: ImageVector, @StringRes textId: Int, onClick: () -> Unit) {
127+
Row(
128+
verticalAlignment = Alignment.CenterVertically,
129+
modifier = Modifier
130+
.clip(RoundedCornerShape(16.dp))
131+
.clickable(onClick = onClick)
132+
.padding(8.dp)
133+
) {
134+
Row(
135+
modifier = Modifier
136+
.fillMaxWidth()
137+
.weight(1f),
138+
verticalAlignment = Alignment.CenterVertically,
139+
horizontalArrangement = Arrangement.spacedBy(12.dp)
140+
) {
141+
GmTonalIconButton(icon = icon)
142+
Text(text = stringResource(id = textId), style = MaterialTheme.typography.titleMedium)
143+
}
144+
Icon(
145+
imageVector = GmIcons.NavigateNextDefault,
146+
contentDescription = null,
147+
)
148+
}
149+
}
33150

34-
}
151+
@Preview(showBackground = true)
152+
@Composable
153+
fun ProfilePreview() {
154+
GptmapTheme {
155+
ProfileScreen(
156+
user = User(
157+
userId = "sumo",
158+
fullName = "Selena Beard",
159+
email = "[email protected]",
160+
profilePictureUrl = "https://search.yahoo.com/search?p=vestibulum",
161+
fcmToken = "perpetua",
162+
provider = "iisque"
163+
),
164+
{}, {}, {}
165+
)
166+
}
167+
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
package com.espressodev.gptmap.feature.profile
22

3-
class ProfileUiState {
4-
}
3+
data class ProfileUiState(
4+
val email: String = "",
5+
val name: String = ""
6+
)
Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,67 @@
11
package com.espressodev.gptmap.feature.profile
22

3+
import androidx.lifecycle.viewModelScope
34
import com.espressodev.gptmap.core.common.GmViewModel
5+
import com.espressodev.gptmap.core.data.AccountService
6+
import com.espressodev.gptmap.core.data.FirestoreService
47
import com.espressodev.gptmap.core.data.LogService
8+
import com.espressodev.gptmap.core.model.Exceptions.FirebaseUserIsNullException
9+
import com.espressodev.gptmap.core.model.Response
10+
import com.espressodev.gptmap.core.model.User
11+
import com.espressodev.gptmap.core.mongodb.RealmAccountService
512
import dagger.hilt.android.lifecycle.HiltViewModel
13+
import kotlinx.coroutines.CancellationException
14+
import kotlinx.coroutines.CoroutineDispatcher
15+
import kotlinx.coroutines.delay
16+
import kotlinx.coroutines.flow.SharingStarted
17+
import kotlinx.coroutines.flow.catch
18+
import kotlinx.coroutines.flow.flowOn
19+
import kotlinx.coroutines.flow.map
20+
import kotlinx.coroutines.flow.mapNotNull
21+
import kotlinx.coroutines.flow.retryWhen
22+
import kotlinx.coroutines.flow.stateIn
623
import javax.inject.Inject
724

825
@HiltViewModel
9-
class ProfileViewModel @Inject constructor(logService: LogService): GmViewModel(logService) {
26+
class ProfileViewModel @Inject constructor(
27+
private val accountService: AccountService,
28+
private val realmAccountService: RealmAccountService,
29+
private val firestoreService: FirestoreService,
30+
ioDispatcher: CoroutineDispatcher,
31+
logService: LogService
32+
) : GmViewModel(logService) {
33+
val user = firestoreService
34+
.getUserFlow(accountService.userId)
35+
.retryWhen { cause, attempt ->
36+
if (cause is CancellationException && attempt < MAX_RETRY_ATTEMPTS) {
37+
delay(RETRY_DELAY_MS)
38+
true
39+
} else {
40+
false
41+
}
42+
}
43+
.mapNotNull { it }
44+
.map<User, Response<User>> { Response.Success(it) }
45+
.catch { exception ->
46+
logService.logNonFatalCrash(exception)
47+
emit(Response.Failure(FirebaseUserIsNullException()))
48+
}
49+
.flowOn(ioDispatcher)
50+
.stateIn(
51+
viewModelScope,
52+
SharingStarted.WhileSubscribed(5000),
53+
Response.Loading
54+
)
55+
56+
57+
fun onLogoutClick(navigate: () -> Unit) = launchCatching {
58+
accountService.signOut()
59+
realmAccountService.logOut()
60+
navigate()
61+
}
62+
63+
companion object {
64+
private const val MAX_RETRY_ATTEMPTS = 3L
65+
private const val RETRY_DELAY_MS = 2000L
66+
}
1067
}

0 commit comments

Comments
 (0)