pref: 优化性能

This commit is contained in:
HuanCheng65 2023-07-23 15:57:21 +08:00
parent dbd07caaec
commit b224836d0f
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
15 changed files with 454 additions and 390 deletions

View File

@ -2,6 +2,7 @@ package com.huanchengfly.tieba.post.api.models
import com.google.gson.annotations.SerializedName
import com.huanchengfly.tieba.post.models.BaseBean
import javax.annotation.concurrent.Immutable
data class ThreadStoreBean(
@SerializedName("error_code")
@ -10,6 +11,7 @@ data class ThreadStoreBean(
@SerializedName("store_thread")
val storeThread: List<ThreadStoreInfo>? = null
) : BaseBean() {
@Immutable
data class ThreadStoreInfo(
@SerializedName("thread_id")
val threadId: String,

View File

@ -54,6 +54,7 @@ fun <T : UiState, A> Flow<T>.collectPartialAsState(
this@collectPartialAsState
.map { prop1.get(it) }
.distinctUntilChanged()
.flowOn(Dispatchers.IO)
.collect {
value = it
}
@ -71,6 +72,7 @@ inline fun <reified Event : UiEvent> Flow<UiEvent>.onEvent(
this@onEvent
.filterIsInstance<Event>()
.cancellable()
.flowOn(Dispatchers.IO)
.collect {
launch {
listener(it)
@ -95,6 +97,7 @@ inline fun <reified Event : UiEvent> BaseViewModel<*, *, *, *>.onEvent(
uiEventFlow
.filterIsInstance<Event>()
.cancellable()
.flowOn(Dispatchers.IO)
.collect {
coroutineScope.launch {
listener(it)
@ -119,6 +122,7 @@ inline fun <reified VM : BaseViewModel<*, *, *, *>> pageViewModel(): VM {
uiEventFlow
.filterIsInstance<CommonUiEvent>()
.cancellable()
.flowOn(Dispatchers.IO)
.collectIn(context) {
context.handleCommonEvent(it)
}

View File

@ -17,6 +17,8 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Dialog
import com.huanchengfly.tieba.post.ui.widgets.compose.DialogNegativeButton
import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.launch
/**
@ -92,6 +94,9 @@ fun ListPref(
onClick = { if (enabled) dialogState.show() },
)
val itemTitle = remember(entries) { entries.map { it.value }.toImmutableList() }
val itemValues = remember(entries) { entries.map { it.key }.toImmutableList() }
Dialog(
dialogState = dialogState,
title = { Text(text = title) },
@ -100,15 +105,15 @@ fun ListPref(
}
) {
ListSinglePicker(
itemTitles = entries.map { it.value },
itemValues = entries.map { it.key },
itemTitles = itemTitle,
itemValues = itemValues,
selectedPosition = entries.keys.indexOf(selected),
onItemSelected = { _, title, value, _ ->
edit(current = value to title)
dismiss()
},
itemIcons = icons,
modifier = Modifier.padding(bottom = 16.dp)
modifier = Modifier.padding(bottom = 16.dp),
itemIcons = icons.toImmutableMap()
)
}
// if (showDialog) {

View File

@ -14,7 +14,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -24,6 +23,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
import com.huanchengfly.tieba.post.ui.page.history.list.HistoryListPage
@ -36,7 +36,6 @@ import com.huanchengfly.tieba.post.utils.HistoryUtil
import com.ramcosta.composedestinations.annotation.DeepLink
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@ -55,8 +54,6 @@ fun HistoryPage(
val context = LocalContext.current
val eventFlow = remember { MutableSharedFlow<HistoryListUiEvent>() }
MyScaffold(
backgroundColor = Color.Transparent,
scaffoldState = scaffoldState,
@ -70,7 +67,7 @@ fun HistoryPage(
IconButton(onClick = {
coroutineScope.launch {
HistoryUtil.deleteAll()
eventFlow.emit(HistoryListUiEvent.DeleteAll)
emitGlobalEvent(HistoryListUiEvent.DeleteAll)
launch {
scaffoldState.snackbarHostState.showSnackbar(
context.getString(
@ -148,9 +145,9 @@ fun HistoryPage(
userScrollEnabled = true,
) {
if (it == 0) {
HistoryListPage(type = HistoryUtil.TYPE_THREAD, eventFlow = eventFlow)
HistoryListPage(type = HistoryUtil.TYPE_THREAD)
} else {
HistoryListPage(type = HistoryUtil.TYPE_FORUM, eventFlow = eventFlow)
HistoryListPage(type = HistoryUtil.TYPE_FORUM)
}
}
}

View File

@ -13,7 +13,6 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -25,6 +24,7 @@ import com.huanchengfly.tieba.post.activities.ForumActivity
import com.huanchengfly.tieba.post.activities.ThreadActivity
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.fromJson
import com.huanchengfly.tieba.post.models.ThreadHistoryInfoBean
@ -45,29 +45,19 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader
import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
import com.huanchengfly.tieba.post.utils.DateTimeUtils
import com.huanchengfly.tieba.post.utils.HistoryUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun HistoryListPage(
type: Int,
eventFlow: Flow<HistoryListUiEvent>,
viewModel: HistoryListViewModel = if (type == HistoryUtil.TYPE_THREAD) pageViewModel<ThreadHistoryListViewModel>() else pageViewModel<ForumHistoryListViewModel>()
) {
LazyLoad(loaded = viewModel.initialized) {
viewModel.send(HistoryListUiIntent.Refresh)
viewModel.initialized = true
}
LaunchedEffect(null) {
launch {
eventFlow
.filterIsInstance<HistoryListUiEvent.DeleteAll>()
.collect {
viewModel.send(HistoryListUiIntent.DeleteAll)
}
}
onGlobalEvent<HistoryListUiEvent.DeleteAll> {
viewModel.send(HistoryListUiIntent.DeleteAll)
}
val isLoadingMore by viewModel.uiState.collectPartialAsState(
prop1 = HistoryListUiState::isLoadingMore,

View File

@ -11,6 +11,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -32,27 +33,28 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
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.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.github.panpf.sketch.request.PauseLoadWhenScrollingDrawableDecodeInterceptor
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -124,6 +126,10 @@ fun PersonalizedPage(
) {
viewModel.send(PersonalizedUiIntent.Refresh)
}
viewModel.onEvent<PersonalizedUiEvent.RefreshSuccess> {
refreshCount = it.count
showRefreshTip = true
}
if (showRefreshTip) {
LaunchedEffect(Unit) {
@ -135,14 +141,14 @@ fun PersonalizedPage(
showRefreshTip = false
}
}
if (lazyListState.isScrollInProgress) {
DisposableEffect(Unit) {
PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true
onDispose {
PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false
}
}
}
// if (lazyListState.isScrollInProgress) {
// DisposableEffect(Unit) {
// PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true
// onDispose {
// PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false
// }
// }
// }
Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LoadMoreLayout(
isLoading = isLoadingMore,
@ -150,6 +156,7 @@ fun PersonalizedPage(
onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) },
) {
FeedList(
state = lazyListState,
dataProvider = { data },
personalizedDataProvider = { threadPersonalizedData },
refreshPositionProvider = { refreshPosition },
@ -191,12 +198,10 @@ fun PersonalizedPage(
)
)
},
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) },
onOpenForum = {
navigator.navigate(ForumPageDestination(it))
},
state = lazyListState
)
onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }
) {
navigator.navigate(ForumPageDestination(it))
}
}
PullRefreshIndicator(
@ -213,28 +218,34 @@ fun PersonalizedPage(
exit = slideOutVertically() + fadeOut(),
modifier = Modifier.align(Alignment.TopCenter)
) {
Box(
modifier = Modifier
.padding(top = 72.dp)
.clip(RoundedCornerShape(100))
.background(
color = ExtendedTheme.colors.primary,
shape = RoundedCornerShape(100)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.TopCenter)
) {
Text(
text = stringResource(id = R.string.toast_feed_refresh, refreshCount),
color = ExtendedTheme.colors.onAccent
)
}
RefreshTip(refreshCount = refreshCount)
}
}
}
@Composable
private fun BoxScope.RefreshTip(refreshCount: Int) {
Box(
modifier = Modifier
.padding(top = 72.dp)
.clip(RoundedCornerShape(100))
.background(
color = ExtendedTheme.colors.primary,
shape = RoundedCornerShape(100)
)
.padding(horizontal = 16.dp, vertical = 8.dp)
.align(Alignment.TopCenter)
) {
Text(
text = stringResource(id = R.string.toast_feed_refresh, refreshCount),
color = ExtendedTheme.colors.onAccent
)
}
}
@Composable
private fun FeedList(
state: LazyListState,
dataProvider: () -> List<ImmutableHolder<ThreadInfo>>,
personalizedDataProvider: () -> List<ImmutableHolder<ThreadPersonalized>?>,
refreshPositionProvider: () -> Int,
@ -245,16 +256,19 @@ private fun FeedList(
onDislike: (ThreadInfo, Long, List<ImmutableHolder<DislikeReason>>) -> Unit,
onRefresh: () -> Unit,
onOpenForum: (forumName: String) -> Unit = {},
state: LazyListState,
) {
val data = dataProvider()
val threadPersonalizedData = personalizedDataProvider()
val refreshPosition = refreshPositionProvider()
val hiddenThreadIds = hiddenThreadIdsProvider()
val windowSizeClass = BaseComposeActivity.LocalWindowSizeClass.current
val itemFraction = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> 0.5f
else -> 1f
val windowWidthSizeClass by rememberUpdatedState(newValue = LocalWindowSizeClass.current.widthSizeClass)
val itemFraction by remember {
derivedStateOf {
when (windowWidthSizeClass) {
WindowWidthSizeClass.Expanded -> 0.5f
else -> 1f
}
}
}
LazyColumn(
state = state,
@ -273,11 +287,23 @@ private fun FeedList(
}
}
) { index, item ->
val isHidden =
remember(hiddenThreadIds, item) { hiddenThreadIds.contains(item.get { threadId }) }
val personalized =
remember(threadPersonalizedData, index) { threadPersonalizedData.getOrNull(index) }
val isRefreshPosition =
remember(index, refreshPosition) { index + 1 == refreshPosition }
val isNotLast = remember(index, data.size) { index < data.size - 1 }
val showDivider = remember(
isHidden,
isRefreshPosition,
isNotLast
) { !isHidden && !isRefreshPosition && isNotLast }
Column(
modifier = Modifier.fillMaxWidth(itemFraction)
) {
AnimatedVisibility(
visible = !hiddenThreadIds.contains(item.get { threadId }),
visible = !isHidden,
enter = EnterTransition.None,
exit = shrinkVertically() + fadeOut()
) {
@ -290,8 +316,6 @@ private fun FeedList(
onOpenForum(it.name)
}
) {
val personalized = threadPersonalizedData.getOrNull(index)
if (personalized != null) {
Dislike(
personalized = personalized,
@ -302,35 +326,40 @@ private fun FeedList(
}
}
}
if (!hiddenThreadIds.contains(item.get { threadId })) {
if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) {
VerticalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 2.dp
)
}
if (showDivider) {
VerticalDivider(
modifier = Modifier.padding(horizontal = 16.dp),
thickness = 2.dp
)
}
if (refreshPosition != 0 && index + 1 == refreshPosition) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onRefresh)
.padding(8.dp),
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(id = R.string.tip_refresh),
style = MaterialTheme.typography.subtitle1
)
}
if (isRefreshPosition) {
RefreshTip(onRefresh)
}
}
}
}
}
@Composable
private fun RefreshTip(
onRefresh: () -> Unit
) {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onRefresh)
.padding(8.dp),
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = stringResource(id = R.string.tip_refresh),
style = MaterialTheme.typography.subtitle1
)
}
}

View File

@ -2,7 +2,6 @@ package com.huanchengfly.tieba.post.ui.page.main.home
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -22,7 +21,6 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem
@ -85,6 +83,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Button
import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.MenuState
import com.huanchengfly.tieba.post.ui.widgets.compose.TextButton
import com.huanchengfly.tieba.post.ui.widgets.compose.TipScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
@ -96,6 +95,7 @@ import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.huanchengfly.tieba.post.utils.appPreferences
import kotlinx.collections.immutable.persistentListOf
private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells {
return if (listSingle) {
@ -229,6 +229,112 @@ private fun ForumItemPlaceholder(
}
}
@Composable
private fun ForumItemMenuContent(
menuState: MenuState,
isTopForum: Boolean,
onDeleteTopForum: () -> Unit,
onAddTopForum: () -> Unit,
onCopyName: () -> Unit,
onUnfollow: () -> Unit,
) {
DropdownMenuItem(
onClick = {
if (isTopForum) {
onDeleteTopForum()
} else {
onAddTopForum()
}
menuState.expanded = false
}
) {
if (isTopForum) {
Text(text = stringResource(id = R.string.menu_top_del))
} else {
Text(text = stringResource(id = R.string.menu_top))
}
}
DropdownMenuItem(
onClick = {
onCopyName()
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.title_copy_forum_name))
}
DropdownMenuItem(
onClick = {
onUnfollow()
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.button_unfollow))
}
}
@Composable
private fun ForumItemContent(
item: HomeUiState.Forum,
showAvatar: Boolean
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
AnimatedVisibility(visible = showAvatar) {
Row {
Avatar(data = item.avatar, size = 40.dp, contentDescription = null)
Spacer(modifier = Modifier.width(14.dp))
}
}
Text(
color = ExtendedTheme.colors.text,
text = item.forumName,
modifier = Modifier
.weight(1f)
.align(CenterVertically),
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.width(54.dp)
.background(
color = ExtendedTheme.colors.chip,
shape = RoundedCornerShape(3.dp)
)
.padding(vertical = 4.dp)
.align(CenterVertically)
) {
Row(
modifier = Modifier.align(Center),
) {
Text(
text = "Lv.${item.levelId}",
color = ExtendedTheme.colors.onChip,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(CenterVertically)
)
if (item.isSign) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(id = R.string.tip_signed),
modifier = Modifier
.size(12.dp)
.align(CenterVertically),
tint = ExtendedTheme.colors.onChip
)
}
}
}
}
}
@Composable
private fun ForumItem(
item: HomeUiState.Forum,
@ -243,116 +349,30 @@ private fun ForumItem(
val menuState = rememberMenuState()
LongClickMenu(
menuContent = {
if (isTopForum) {
DropdownMenuItem(
onClick = {
onDeleteTopForum(item)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.menu_top_del))
}
} else {
DropdownMenuItem(
onClick = {
onAddTopForum(item)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.menu_top))
}
}
DropdownMenuItem(
onClick = {
ForumItemMenuContent(
menuState = menuState,
isTopForum = isTopForum,
onDeleteTopForum = { onDeleteTopForum(item) },
onAddTopForum = { onAddTopForum(item) },
onCopyName = {
TiebaUtil.copyText(context, item.forumName)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.title_copy_forum_name))
}
DropdownMenuItem(
onClick = {
onUnfollow(item)
menuState.expanded = false
}
) {
Text(text = stringResource(id = R.string.button_unfollow))
}
},
onUnfollow = { onUnfollow(item) }
)
},
menuState = menuState,
onClick = {
onClick(item)
}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = 12.dp)
.animateContentSize(),
) {
AnimatedVisibility(visible = showAvatar) {
Row {
Avatar(data = item.avatar, size = 40.dp, contentDescription = null)
Spacer(modifier = Modifier.width(14.dp))
}
}
Text(
color = ExtendedTheme.colors.text,
text = item.forumName,
modifier = Modifier
.weight(1f)
.align(CenterVertically),
fontSize = 15.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.width(54.dp)
.background(
color = ExtendedTheme.colors.chip,
shape = RoundedCornerShape(3.dp)
)
.padding(vertical = 4.dp)
.align(CenterVertically)
) {
Row(
modifier = Modifier.align(Center),
) {
Text(
text = "Lv.${item.levelId}",
color = ExtendedTheme.colors.onChip,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.align(CenterVertically)
)
if (item.isSign) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = stringResource(id = R.string.tip_signed),
modifier = Modifier
.size(12.dp)
.align(CenterVertically),
tint = ExtendedTheme.colors.onChip
)
}
}
}
}
ForumItemContent(item = item, showAvatar = showAvatar)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun HomePage(
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
listOf(
HomeUiIntent.Refresh
)
),
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(listOf(HomeUiIntent.Refresh)),
canOpenExplore: Boolean = false,
onOpenExplore: () -> Unit = {},
) {
@ -365,11 +385,11 @@ fun HomePage(
)
val forums by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::forums,
initial = emptyList()
initial = persistentListOf()
)
val topForums by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::topForums,
initial = emptyList()
initial = persistentListOf()
)
val error by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::error,
@ -431,61 +451,92 @@ fun HomePage(
},
modifier = Modifier.fillMaxSize(),
) { contentPaddings ->
StateScreen(
isEmpty = isEmpty,
isError = isError,
isLoading = isLoading,
modifier = Modifier.padding(contentPaddings),
onReload = {
viewModel.send(HomeUiIntent.Refresh)
},
emptyScreen = {
EmptyScreen(
loggedIn = isLoggedIn,
canOpenExplore = canOpenExplore,
onOpenExplore = onOpenExplore
)
},
loadingScreen = {
HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells)
},
errorScreen = {
error?.let { ErrorScreen(error = it) }
}
val pullRefreshState = rememberPullRefreshState(
refreshing = isLoading,
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
.padding(contentPaddings)
) {
val pullRefreshState = rememberPullRefreshState(
refreshing = isLoading,
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
) {
val gridState = rememberLazyGridState()
LazyVerticalGrid(
state = gridState,
columns = gridCells,
contentPadding = PaddingValues(bottom = 12.dp),
modifier = Modifier.fillMaxSize(),
) {
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
context.goToActivity<NewSearchActivity>()
}
Column {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
context.goToActivity<NewSearchActivity>()
}
StateScreen(
isEmpty = isEmpty,
isError = isError,
isLoading = isLoading,
modifier = Modifier.weight(1f),
onReload = {
viewModel.send(HomeUiIntent.Refresh)
},
emptyScreen = {
EmptyScreen(
loggedIn = isLoggedIn,
canOpenExplore = canOpenExplore,
onOpenExplore = onOpenExplore
)
},
loadingScreen = {
HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells)
},
errorScreen = {
error?.let { ErrorScreen(error = it) }
}
if (hasTopForum) {
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column {
Header(
text = stringResource(id = R.string.title_top_forum),
invert = true
) {
LazyVerticalGrid(
columns = gridCells,
contentPadding = PaddingValues(bottom = 12.dp),
modifier = Modifier.fillMaxSize(),
) {
if (hasTopForum) {
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column {
Header(
text = stringResource(id = R.string.title_top_forum),
invert = true
)
Spacer(modifier = Modifier.height(8.dp))
}
}
items(
items = topForums,
key = { "Top${it.forumId}" }
) { item ->
ForumItem(
item,
listSingle,
onClick = {
navigator.navigate(ForumPageDestination(it.forumName))
},
onUnfollow = {
unfollowForum = it
confirmUnfollowDialog.show()
},
onAddTopForum = {
viewModel.send(HomeUiIntent.TopForums.Add(it))
},
onDeleteTopForum = {
viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId))
},
isTopForum = true
)
Spacer(modifier = Modifier.height(8.dp))
}
item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column(
modifier = Modifier.padding(
vertical = 8.dp
)
) {
Header(text = stringResource(id = R.string.forum_list_title))
}
}
}
items(
items = topForums,
key = { "Top${it.forumId}" }
items = forums,
key = { it.forumId }
) { item ->
ForumItem(
item,
@ -502,58 +553,20 @@ fun HomePage(
},
onDeleteTopForum = {
viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId))
},
isTopForum = true
}
)
}
item(
key = "Spacer",
span = { GridItemSpan(maxLineSpan) }) {
Spacer(
modifier = Modifier.height(
16.dp
)
)
}
item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column {
Header(text = stringResource(id = R.string.forum_list_title))
Spacer(modifier = Modifier.height(8.dp))
}
}
}
items(
items = forums,
key = { it.forumId }
) { item ->
ForumItem(
item,
listSingle,
onClick = {
navigator.navigate(ForumPageDestination(it.forumName))
},
onUnfollow = {
unfollowForum = it
confirmUnfollowDialog.show()
},
onAddTopForum = {
viewModel.send(HomeUiIntent.TopForums.Add(it))
},
onDeleteTopForum = {
viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId))
}
)
}
}
PullRefreshIndicator(
refreshing = isLoading,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = ExtendedTheme.colors.pullRefreshIndicator,
contentColor = ExtendedTheme.colors.primary,
)
}
PullRefreshIndicator(
refreshing = isLoading,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = ExtendedTheme.colors.pullRefreshIndicator,
contentColor = ExtendedTheme.colors.primary,
)
}
}
}
@ -570,11 +583,6 @@ private fun HomePageSkeletonScreen(
modifier = Modifier
.fillMaxSize(),
) {
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
context.goToActivity<NewSearchActivity>()
}
}
item(key = "TopForumHeaderPlaceholder", span = { GridItemSpan(maxLineSpan) }) {
Column {
Header(

View File

@ -6,12 +6,29 @@ import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.*
import com.huanchengfly.tieba.post.arch.BaseViewModel
import com.huanchengfly.tieba.post.arch.CommonUiEvent
import com.huanchengfly.tieba.post.arch.PartialChange
import com.huanchengfly.tieba.post.arch.PartialChangeProducer
import com.huanchengfly.tieba.post.arch.UiEvent
import com.huanchengfly.tieba.post.arch.UiIntent
import com.huanchengfly.tieba.post.arch.UiState
import com.huanchengfly.tieba.post.models.database.TopForum
import com.huanchengfly.tieba.post.utils.AccountUtil
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import org.litepal.LitePal
@Stable
@ -114,8 +131,10 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
when (this) {
is Success -> {
oldState.copy(
forums = oldState.forums.filterNot { it.forumId == forumId },
topForums = oldState.topForums.filterNot { it.forumId == forumId },
forums = oldState.forums.filterNot { it.forumId == forumId }
.toImmutableList(),
topForums = oldState.topForums.filterNot { it.forumId == forumId }
.toImmutableList(),
)
}
@ -132,8 +151,8 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
when (this) {
is Success -> oldState.copy(
isLoading = false,
forums = forums,
topForums = topForums,
forums = forums.toImmutableList(),
topForums = topForums.toImmutableList(),
error = null
)
@ -157,7 +176,9 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
sealed interface Delete : HomePartialChange {
override fun reduce(oldState: HomeUiState): HomeUiState =
when (this) {
is Success -> oldState.copy(topForums = oldState.topForums.filterNot { it.forumId == forumId })
is Success -> oldState.copy(topForums = oldState.topForums.filterNot { it.forumId == forumId }
.toImmutableList())
is Failure -> oldState
}
@ -174,6 +195,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
topForumsId.add(forum.forumId)
oldState.copy(
topForums = oldState.forums.filter { topForumsId.contains(it.forumId) }
.toImmutableList()
)
}
@ -190,8 +212,8 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
@Immutable
data class HomeUiState(
val isLoading: Boolean = true,
val forums: List<Forum> = emptyList(),
val topForums: List<Forum> = emptyList(),
val forums: ImmutableList<Forum> = persistentListOf(),
val topForums: ImmutableList<Forum> = persistentListOf(),
val error: Throwable? = null,
) : UiState {
@Immutable

View File

@ -35,6 +35,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.huanchengfly.tieba.post.App
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.UserActivity
@ -595,7 +596,7 @@ private fun SubPostItem(
.padding(start = Sizes.Small + 8.dp)
.fillMaxWidth()
) {
contentRenders.forEach { it.Render() }
contentRenders.fastForEach { it.Render() }
}
}
)

View File

@ -84,6 +84,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.huanchengfly.tieba.post.App
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.UserActivity
@ -1683,7 +1685,7 @@ fun PostCard(
)
}
contentRenders.forEach { it.Render() }
contentRenders.fastForEach { it.Render() }
}
if (showSubPosts && post.sub_post_number > 0 && subPostContents.isNotEmpty() && !immersiveMode) {
@ -1696,7 +1698,7 @@ fun PostCard(
.padding(vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
subPostContents.forEachIndexed { index, text ->
subPostContents.fastForEachIndexed { index, text ->
SubPostItem(
subPostList = subPosts[index],
subPostContent = text,

View File

@ -54,10 +54,12 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEachIndexed
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.huanchengfly.tieba.post.App
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.UserActivity
import com.huanchengfly.tieba.post.api.models.protos.Media
@ -85,14 +87,14 @@ import kotlin.math.max
import kotlin.math.min
private val ImmutableHolder<Media>.url: String
@Composable get() =
ImageUtil.getUrl(
LocalContext.current,
true,
get { originPic },
get { dynamicPic },
get { bigPic },
get { srcPic })
get() = ImageUtil.getUrl(
App.INSTANCE,
true,
get { originPic },
get { dynamicPic },
get { bigPic },
get { srcPic }
)
@Composable
private fun DefaultUserHeader(
@ -329,53 +331,65 @@ private fun ThreadMedia(
val medias = remember(item) {
item.getImmutableList { media }
}
val singleMediaFraction =
if (LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact)
val hasMedia = remember(medias) { medias.isNotEmpty() }
val isSingleMedia = remember(medias) { medias.size == 1 }
val windowWidthSizeClass = LocalWindowSizeClass.current.widthSizeClass
val singleMediaFraction = remember(windowWidthSizeClass) {
if (windowWidthSizeClass == WindowWidthSizeClass.Compact)
1f
else 0.6f
}
if (isVideo) {
val videoInfo = item.getImmutable { videoInfo!! }
val videoInfo = remember(item) { item.getImmutable { videoInfo!! } }
val aspectRatio = remember(videoInfo) {
max(
videoInfo
.get { thumbnailWidth }
.toFloat() / videoInfo.get { thumbnailHeight },
16f / 9
)
}
VideoPlayer(
videoUrl = videoInfo.get { videoUrl },
thumbnailUrl = videoInfo.get { thumbnailUrl },
modifier = Modifier
.fillMaxWidth(singleMediaFraction)
.aspectRatio(
max(
videoInfo
.get { thumbnailWidth }
.toFloat() / videoInfo.get { thumbnailHeight },
16f / 9
)
)
.aspectRatio(aspectRatio)
.clip(RoundedCornerShape(8.dp))
)
} else if (medias.isNotEmpty()) {
} else if (hasMedia) {
val mediaWidthFraction = remember(isSingleMedia, singleMediaFraction) {
if (isSingleMedia) singleMediaFraction else 1f
}
val mediaAspectRatio = remember(isSingleMedia) {
if (isSingleMedia) 2f else 3f
}
val showMediaCount = remember(medias) { min(medias.size, 3) }
val hasMoreMedia = remember(medias) { medias.size > 3 }
val showMedias = remember(medias) { medias.subList(0, showMediaCount) }
Box {
Row(
modifier = Modifier
.fillMaxWidth(if (medias.size == 1) singleMediaFraction else 1f)
.aspectRatio(if (medias.size == 1) 2f else 3f)
.fillMaxWidth(mediaWidthFraction)
.aspectRatio(mediaAspectRatio)
.clip(RoundedCornerShape(8.dp)),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
medias.subList(0, min(medias.size, 3))
.forEachIndexed { index, media ->
val photoViewData = remember(item, index) {
getImmutablePhotoViewData(item.get(), index)
}
NetworkImage(
imageUri = media.url,
contentDescription = null,
modifier = Modifier.weight(1f),
photoViewData = photoViewData,
contentScale = ContentScale.Crop
)
showMedias.fastForEachIndexed { index, media ->
val photoViewData = remember(item, index) {
getImmutablePhotoViewData(item.get(), index)
}
NetworkImage(
imageUri = remember(media) { media.url },
contentDescription = null,
modifier = Modifier.weight(1f),
photoViewData = photoViewData,
contentScale = ContentScale.Crop
)
}
}
if (medias.size > 3) {
if (hasMoreMedia) {
Badge(
icon = Icons.Rounded.PhotoSizeSelectActual,
text = "${medias.size}",
@ -396,7 +410,8 @@ private fun ThreadForumInfo(
val hasForumInfo = remember(item) { item.isNotNull { forumInfo } }
if (hasForumInfo) {
val forumInfo = remember(item) { item.getImmutable { forumInfo!! } }
if (forumInfo.get { name }.isNotBlank()) {
val hasForum = remember(forumInfo) { forumInfo.get { name }.isNotBlank() }
if (hasForum) {
ForumInfoChip(
imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) },
nameProvider = { forumInfo.get { name } },

View File

@ -4,7 +4,9 @@ import android.os.Parcelable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
@ -38,11 +40,14 @@ fun NetworkImage(
}
}
} else Modifier
AsyncImage(
request = DisplayRequest(context, imageUri) {
val request = remember(imageUri) {
DisplayRequest(context, imageUri) {
placeholder(ImageUtil.getPlaceHolder(context, 0))
crossfade()
},
}
}
AsyncImage(
request = request,
contentDescription = contentDescription,
modifier = modifier.then(clickableModifier),
contentScale = contentScale,
@ -57,29 +62,14 @@ fun NetworkImage(
photoViewDataProvider: (() -> ImmutableHolder<PhotoViewData>)? = null,
contentScale: ContentScale = ContentScale.Fit,
) {
val imageUri = imageUriProvider()
val photoViewData = photoViewDataProvider?.invoke()
val imageUri by rememberUpdatedState(newValue = imageUriProvider())
val photoViewData by rememberUpdatedState(newValue = photoViewDataProvider?.invoke())
val context = LocalContext.current
val clickableModifier = if (photoViewData != null) {
Modifier.clickable(
indication = null,
interactionSource = remember {
MutableInteractionSource()
}
) {
context.goToActivity<PhotoViewActivity> {
putExtra(EXTRA_PHOTO_VIEW_DATA, photoViewData.get() as Parcelable)
}
}
} else Modifier
AsyncImage(
request = DisplayRequest(context, imageUri) {
placeholder(ImageUtil.getPlaceHolder(context, 0))
crossfade()
},
NetworkImage(
imageUri = imageUri,
contentDescription = contentDescription,
modifier = modifier.then(clickableModifier),
modifier = modifier,
photoViewData = photoViewData,
contentScale = contentScale,
)
}

View File

@ -20,7 +20,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
@ -118,16 +117,15 @@ fun LongClickMenu(
indication: Indication? = LocalIndication.current,
content: @Composable () -> Unit
) {
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = null) {
coroutineScope.launch {
launch {
interactionSource.interactions
.filterIsInstance<PressInteraction.Press>()
.collect {
menuState.offset = it.pressPosition
}
}
coroutineScope.launch {
launch {
interactionSource.interactions
.collect {
Log.i("Indication", "$it")
@ -172,18 +170,15 @@ fun LongClickMenu(
@Composable
fun rememberMenuState(): MenuState {
return rememberSaveable(saver = MenuState.Saver) {
MenuState()
}
return rememberSaveable(
saver = MenuState.Saver,
init = { MenuState() }
)
}
@Stable
class MenuState(
expanded: Boolean = false,
offsetX: Float = 0f,
offsetY: Float = 0f,
) {
private var _expanded by mutableStateOf(expanded)
class MenuState {
private var _expanded by mutableStateOf(false)
var expanded: Boolean
get() = _expanded
@ -193,7 +188,7 @@ class MenuState(
}
}
private var _offset by mutableStateOf(Offset(offsetX, offsetY))
private var _offset by mutableStateOf(Offset(0f, 0f))
var offset: Offset
get() = _offset
@ -213,11 +208,10 @@ class MenuState(
)
},
restore = {
MenuState(
expanded = it[0] as Boolean,
offsetX = it[1] as Float,
offsetY = it[2] as Float,
)
MenuState().apply {
expanded = it[0] as Boolean
offset = Offset(it[1] as Float, it[2] as Float)
}
}
)
}

View File

@ -22,14 +22,18 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf
@Composable
fun <ItemValue> ListSinglePicker(
itemTitles: List<String>,
itemValues: List<ItemValue>,
itemTitles: ImmutableList<String>,
itemValues: ImmutableList<ItemValue>,
selectedPosition: Int,
onItemSelected: (position: Int, title: String, value: ItemValue, changed: Boolean) -> Unit,
itemIcons: Map<ItemValue, @Composable () -> Unit> = emptyMap(),
modifier: Modifier = Modifier,
itemIcons: ImmutableMap<ItemValue, @Composable () -> Unit> = persistentMapOf(),
selectedIndicator: @Composable () -> Unit = {
Icon(
imageVector = Icons.Rounded.Check,
@ -38,7 +42,6 @@ fun <ItemValue> ListSinglePicker(
},
colors: PickerColors = PickerDefaults.pickerColors(),
enabled: Boolean = true,
modifier: Modifier = Modifier,
) {
if (itemTitles.size != itemValues.size) error("titles and values do not match!")
Column(modifier = modifier) {
@ -63,7 +66,7 @@ fun <ItemValue> ListSinglePicker(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
(itemIcons.getOrDefault(itemValues[it]) {}).invoke()
itemIcons[itemValues[it]]?.invoke()
ProvideContentColor(
color = if (selected) colors.selectedItemColor(enabled).value else colors.itemColor(
enabled

View File

@ -7,6 +7,7 @@ import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.staticCompositionLocalOf
@ -29,6 +30,7 @@ import org.litepal.extension.findAllAsync
import org.litepal.extension.findFirst
import java.util.UUID
@Stable
object AccountUtil {
const val TAG = "AccountUtil"
const val ACTION_SWITCH_ACCOUNT = "com.huanchengfly.tieba.post.action.SWITCH_ACCOUNT"