separated screens for friends tab

This commit is contained in:
2025-03-23 19:53:58 +03:00
parent 0eb3146428
commit 8dc47c3fa5
8 changed files with 386 additions and 409 deletions
@@ -165,7 +165,6 @@ fun MainScreen(
) {
friendsScreen(
onError = onError,
navController = navController,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked
)
@@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
// TODO: 13/07/2024, Danil Nikolaev: separate two lists and their pagination
interface FriendsViewModel {
val screenState: StateFlow<FriendsScreenState>
@@ -33,19 +32,11 @@ interface FriendsViewModel {
fun onErrorConsumed()
fun onTabSelected(tabIndex: Int)
fun setScrollIndex(index: Int)
fun setScrollOffset(offset: Int)
fun setScrollIndexOnline(index: Int)
fun setScrollOffsetOnline(offset: Int)
}
class FriendsViewModelImpl(
private val friendsUseCase: FriendsUseCase,
private val userSettings: UserSettings,
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
) : ViewModel(), FriendsViewModel {
abstract class BaseFriendsViewModelImpl : ViewModel(), FriendsViewModel {
override val screenState = MutableStateFlow(FriendsScreenState.EMPTY)
@@ -54,13 +45,7 @@ class FriendsViewModelImpl(
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
private val friends = MutableStateFlow<List<VkUser>>(emptyList())
init {
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
loadFriends()
}
protected val friends = MutableStateFlow<List<VkUser>>(emptyList())
override fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.friends.size }
@@ -76,10 +61,6 @@ class FriendsViewModelImpl(
baseError.setValue { null }
}
override fun onTabSelected(tabIndex: Int) {
screenState.setValue { old -> old.copy(selectedTabIndex = tabIndex) }
}
override fun setScrollIndex(index: Int) {
screenState.setValue { old -> old.copy(scrollIndex = index) }
}
@@ -88,38 +69,75 @@ class FriendsViewModelImpl(
screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
override fun setScrollIndexOnline(index: Int) {
screenState.setValue { old -> old.copy(scrollIndexOnline = index) }
abstract fun loadFriends(offset: Int = currentOffset.value)
protected fun handleError(error: State.Error) {
when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
override fun setScrollOffsetOnline(offset: Int) {
screenState.setValue { old -> old.copy(scrollOffsetOnline = offset) }
else -> {
baseError.setValue {
BaseError.SimpleError(message = error.errorMessage)
}
}
}
}
State.Error.ConnectionError -> {
baseError.setValue {
BaseError.SimpleError(message = "Connection error")
}
}
State.Error.InternalError -> {
baseError.setValue {
BaseError.SimpleError(message = "Internal error")
}
}
State.Error.UnknownError -> {
baseError.setValue {
BaseError.SimpleError(message = "Unknown error")
}
}
else -> Unit
}
}
protected fun updateFriendsNames(useContactNames: Boolean) {
val friends = friends.value
if (friends.isEmpty()) return
val uiFriends = friends.map { conversation ->
conversation.asPresentation(useContactNames)
}
private fun loadFriends(offset: Int = currentOffset.value) {
friendsUseCase.getOnlineFriends(null, null)
.listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { userIds ->
loadUsersByIdsUseCase(userIds = userIds)
.listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { onlineFriends ->
screenState.setValue { old ->
old.copy(
onlineFriends = onlineFriends.map {
it.asPresentation(userSettings.useContactNames.value)
}
)
old.copy(friends = uiFriends)
}
}
)
companion object {
const val LOAD_COUNT = 30
}
}
class FriendsViewModelImpl(
private val friendsUseCase: FriendsUseCase,
private val userSettings: UserSettings
) : BaseFriendsViewModelImpl() {
init {
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
loadFriends()
}
)
}
override fun loadFriends(offset: Int) {
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
.listenValue(viewModelScope) { state ->
state.processState(
@@ -167,62 +185,48 @@ class FriendsViewModelImpl(
}
}
}
}
private fun handleError(error: State.Error) {
when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> {
baseError.setValue {
BaseError.SimpleError(message = error.errorMessage)
}
}
}
}
State.Error.ConnectionError -> {
baseError.setValue {
BaseError.SimpleError(message = "Connection error")
}
}
State.Error.InternalError -> {
baseError.setValue {
BaseError.SimpleError(message = "Internal error")
}
}
State.Error.UnknownError -> {
baseError.setValue {
BaseError.SimpleError(message = "Unknown error")
}
}
else -> Unit
}
}
private fun updateFriendsNames(useContactNames: Boolean) {
val friends = friends.value
if (friends.isEmpty()) return
val uiFriends = friends.map { conversation ->
conversation.asPresentation(useContactNames)
}
val onlineUiFriends = screenState.value.onlineFriends.mapNotNull { friend ->
uiFriends.find { it.userId == friend.userId }
class OnlineFriendsViewModelImpl(
private val friendsUseCase: FriendsUseCase,
private val userSettings: UserSettings,
private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase
) : BaseFriendsViewModelImpl() {
init {
userSettings.useContactNames.listenValue(viewModelScope, ::updateFriendsNames)
loadFriends()
}
override fun loadFriends(offset: Int) {
friendsUseCase.getOnlineFriends(null, null)
.listenValue(viewModelScope) { onlineState ->
onlineState.processState(
error = ::handleError,
success = { userIds ->
loadUsersByIdsUseCase(userIds = userIds).listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
success = { onlineFriends ->
screenState.setValue { old ->
old.copy(
friends = uiFriends,
onlineFriends = onlineUiFriends
friends = onlineFriends.map {
it.asPresentation(userSettings.useContactNames.value)
}
)
}
}
)
companion object {
const val LOAD_COUNT = 15
screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && (onlineState.isLoading() || state.isLoading()),
isPaginating = offset > 0 && (onlineState.isLoading() || state.isLoading())
)
}
}
}
)
}
}
}
@@ -1,8 +1,9 @@
package dev.meloda.fast.friends.di
import dev.meloda.fast.domain.FriendsUseCase
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.domain.FriendsUseCaseImpl
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.bind
@@ -11,4 +12,5 @@ import org.koin.dsl.module
val friendsModule = module {
singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class
viewModelOf(::FriendsViewModelImpl)
viewModelOf(::OnlineFriendsViewModelImpl)
}
@@ -7,28 +7,20 @@ import dev.meloda.fast.ui.model.api.UiFriend
data class FriendsScreenState(
val isLoading: Boolean,
val friends: List<UiFriend>,
val onlineFriends: List<UiFriend>,
val isPaginating: Boolean,
val isPaginationExhausted: Boolean,
val selectedTabIndex: Int,
val scrollIndex: Int,
val scrollOffset: Int,
val scrollIndexOnline: Int,
val scrollOffsetOnline: Int
) {
companion object {
val EMPTY: FriendsScreenState = FriendsScreenState(
isLoading = true,
friends = emptyList(),
onlineFriends = emptyList(),
isPaginating = false,
isPaginationExhausted = false,
selectedTabIndex = 0,
scrollIndex = 0,
scrollOffset = 0,
scrollIndexOnline = 0,
scrollOffsetOnline = 0,
scrollOffset = 0
)
}
}
@@ -1,13 +1,9 @@
package dev.meloda.fast.friends.navigation
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.presentation.FriendsRoute
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.extensions.sharedViewModel
import kotlinx.serialization.Serializable
@Serializable
@@ -15,17 +11,12 @@ object Friends
fun NavGraphBuilder.friendsScreen(
onError: (BaseError) -> Unit,
navController: NavController,
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit
) {
composable<Friends> {
val viewModel: FriendsViewModel =
it.sharedViewModel<FriendsViewModelImpl>(navController = navController)
FriendsRoute(
onError = onError,
viewModel = viewModel,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked
)
@@ -1,81 +1,64 @@
package dev.meloda.fast.friends.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import coil.request.ImageRequest
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.friends.FriendsViewModel
import dev.meloda.fast.friends.FriendsViewModelImpl
import dev.meloda.fast.friends.model.FriendsScreenState
import dev.meloda.fast.friends.OnlineFriendsViewModelImpl
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.ErrorView
import dev.meloda.fast.ui.components.FullScreenLoader
import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.model.TabItem
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import org.koin.androidx.compose.koinViewModel
import dev.meloda.fast.ui.R as UiR
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FriendsRoute(
onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit,
viewModel: FriendsViewModel = koinViewModel<FriendsViewModelImpl>()
fun FriendsScreen(
modifier: Modifier = Modifier,
padding: PaddingValues,
tabIndex: Int,
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userId: Int) -> Unit = {},
setCanScrollBackward: (Boolean) -> Unit = {}
) {
val context = LocalContext.current
val context: Context = LocalContext.current
val viewModel: FriendsViewModel =
if (tabIndex == 0) {
koinViewModel<FriendsViewModelImpl>()
} else {
koinViewModel<OnlineFriendsViewModelImpl>()
}
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle()
@@ -92,43 +75,6 @@ fun FriendsRoute(
}
}
FriendsScreen(
screenState = screenState,
baseError = baseError,
canPaginate = canPaginate,
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefresh = viewModel::onRefresh,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked,
setSelectedTabIndex = viewModel::onTabSelected,
setScrollIndex = viewModel::setScrollIndex,
setScrollOffset = viewModel::setScrollOffset,
setScrollIndexOnline = viewModel::setScrollIndexOnline,
setScrollOffsetOnline = viewModel::setScrollOffsetOnline
)
}
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun FriendsScreen(
screenState: FriendsScreenState = FriendsScreenState.EMPTY,
baseError: BaseError? = null,
canPaginate: Boolean = false,
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
onPaginationConditionsMet: () -> Unit = {},
onRefresh: () -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userId: Int) -> Unit = {},
setSelectedTabIndex: (Int) -> Unit = {},
setScrollIndex: (Int) -> Unit = {},
setScrollOffset: (Int) -> Unit = {},
setScrollIndexOnline: (Int) -> Unit = {},
setScrollOffsetOnline: (Int) -> Unit = {}
) {
val currentTheme = LocalThemeConfig.current
val maxLines by remember {
@@ -141,33 +87,17 @@ fun FriendsScreen(
initialFirstVisibleItemIndex = screenState.scrollIndex,
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
)
val listStateOnline = rememberLazyListState(
initialFirstVisibleItemIndex = screenState.scrollIndexOnline,
initialFirstVisibleItemScrollOffset = screenState.scrollOffsetOnline
)
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.debounce(500L)
.collectLatest(setScrollIndex)
.debounce(250L)
.collectLatest(viewModel::setScrollIndex)
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemScrollOffset }
.debounce(500L)
.collectLatest(setScrollOffset)
}
LaunchedEffect(listStateOnline) {
snapshotFlow { listStateOnline.firstVisibleItemIndex }
.debounce(500L)
.collectLatest(setScrollIndexOnline)
}
LaunchedEffect(listStateOnline) {
snapshotFlow { listStateOnline.firstVisibleItemScrollOffset }
.debounce(500L)
.collectLatest(setScrollOffsetOnline)
.debounce(250L)
.collectLatest(viewModel::setScrollOffset)
}
val paginationConditionMet by remember(canPaginate, listState) {
@@ -180,164 +110,49 @@ fun FriendsScreen(
LaunchedEffect(paginationConditionMet) {
if (paginationConditionMet && !screenState.isPaginating) {
onPaginationConditionsMet()
viewModel.onPaginationConditionsMet()
}
}
val hazeState = LocalHazeState.current
var canScrollBackward by remember {
mutableStateOf(false)
}
val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val topBarContainerColor by animateColorAsState(
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val tabItems = remember {
listOf(
TabItem(
titleResId = UiR.string.title_friends_all,
unselectedIconResId = null,
selectedIconResId = null
),
TabItem(
titleResId = UiR.string.title_friends_online,
unselectedIconResId = null,
selectedIconResId = null
)
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}
)
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.fillMaxWidth()
) {
TopAppBar(
title = {
Text(
text = stringResource(id = UiR.string.title_friends),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth()
)
PrimaryTabRow(
selectedTabIndex = screenState.selectedTabIndex,
modifier = Modifier,
containerColor = Color.Transparent
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = index == screenState.selectedTabIndex,
onClick = {
if (screenState.selectedTabIndex != index) {
setSelectedTabIndex(index)
}
},
text = {
item.titleResId?.let { resId ->
Text(text = stringResource(id = resId))
}
}
)
}
}
}
}
) { padding ->
when {
baseError != null -> {
when (baseError) {
baseError?.let { error ->
when (error) {
is BaseError.SessionExpired -> {
ErrorView(
text = stringResource(UiR.string.session_expired),
buttonText = stringResource(UiR.string.action_log_out),
text = stringResource(R.string.session_expired),
buttonText = stringResource(R.string.action_log_out),
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = stringResource(UiR.string.try_again),
onButtonClick = onRefresh
text = error.message,
buttonText = stringResource(R.string.try_again),
onButtonClick = viewModel::onRefresh
)
}
}
return
}
when {
screenState.isLoading && screenState.friends.isEmpty() -> FullScreenLoader()
else -> {
val pagerState = rememberPagerState(
initialPage = screenState.selectedTabIndex
) {
tabItems.size
}
LaunchedEffect(screenState.selectedTabIndex) {
pagerState.animateScrollToPage(screenState.selectedTabIndex)
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.collect(setSelectedTabIndex)
}
val pullToRefreshState = rememberPullToRefreshState()
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { index ->
PullToRefreshBox(
modifier = Modifier
modifier = modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
.padding(bottom = padding.calculateBottomPadding()),
state = pullToRefreshState,
isRefreshing = screenState.isLoading,
onRefresh = onRefresh,
onRefresh = viewModel::onRefresh,
indicator = {
PullToRefreshDefaults.Indicator(
state = pullToRefreshState,
@@ -348,14 +163,6 @@ fun FriendsScreen(
)
}
) {
val friendsToDisplay = remember(index) {
if (index == 0) {
screenState.friends
} else {
screenState.onlineFriends
}
}
FriendsList(
modifier = if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
@@ -363,28 +170,23 @@ fun FriendsScreen(
Modifier
}.fillMaxSize(),
screenState = screenState,
uiFriends = ImmutableList.copyOf(friendsToDisplay),
listState = if (index == 0) listState else listStateOnline,
uiFriends = ImmutableList.copyOf(screenState.friends),
listState = listState,
maxLines = maxLines,
padding = padding,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked,
setCanScrollBackward = { can ->
canScrollBackward = can
}
setCanScrollBackward = setCanScrollBackward
)
if (friendsToDisplay.isEmpty()) {
if (screenState.friends.isEmpty()) {
NoItemsView(
customText = if (index == 1) stringResource(UiR.string.no_online_friends) else null,
buttonText = stringResource(UiR.string.action_refresh),
onButtonClick = onRefresh
customText = if (tabIndex == 1) stringResource(R.string.no_online_friends) else null,
buttonText = stringResource(R.string.action_refresh),
onButtonClick = viewModel::onRefresh
)
}
}
}
}
}
}
}
}
@@ -0,0 +1,187 @@
package dev.meloda.fast.friends.presentation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.TabItem
import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalThemeConfig
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
@Composable
fun FriendsRoute(
onError: (BaseError) -> Unit,
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userId: Int) -> Unit,
) {
var selectedTabIndex by rememberSaveable {
mutableIntStateOf(0)
}
val currentTheme = LocalThemeConfig.current
val hazeState = LocalHazeState.current
var canScrollBackward by remember {
mutableStateOf(false)
}
val topBarContainerColorAlpha by animateFloatAsState(
targetValue = if (!currentTheme.enableBlur || !canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val topBarContainerColor by animateColorAsState(
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(
durationMillis = 200,
easing = FastOutLinearInEasing
)
)
val tabItems = remember {
listOf(
TabItem(
titleResId = R.string.title_friends_all,
unselectedIconResId = null,
selectedIconResId = null
),
TabItem(
titleResId = R.string.title_friends_online,
unselectedIconResId = null,
selectedIconResId = null
)
)
}
Scaffold(
modifier = Modifier.fillMaxSize(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
Column(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.thick()
)
} else {
Modifier
}
)
.background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha))
.fillMaxWidth()
) {
TopAppBar(
title = {
Text(
text = stringResource(id = R.string.title_friends),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.headlineSmall
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier.fillMaxWidth()
)
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier,
containerColor = Color.Transparent
) {
tabItems.forEachIndexed { index, item ->
Tab(
selected = index == selectedTabIndex,
onClick = {
if (selectedTabIndex != index) {
selectedTabIndex = index
}
},
text = {
item.titleResId?.let { resId ->
Text(text = stringResource(id = resId))
}
}
)
}
}
}
}
) { padding ->
val pagerState = rememberPagerState(
initialPage = selectedTabIndex
) {
tabItems.size
}
LaunchedEffect(selectedTabIndex) {
pagerState.animateScrollToPage(selectedTabIndex)
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.collect { selectedTabIndex = it }
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { index ->
FriendsScreen(
padding = padding,
tabIndex = index,
onSessionExpiredLogOutButtonClicked = { onError(BaseError.SessionExpired) },
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked,
setCanScrollBackward = { canScrollBackward = it }
)
}
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ minSdk = "23"
targetSdk = "35"
compileSdk = "35"
versionCode = "9"
versionName = "0.1.6"
versionName = "0.1.9"
agp = "8.9.0"
converterMoshi = "2.11.0"