pref: 优化首页性能

This commit is contained in:
HuanCheng65 2023-07-22 23:17:59 +08:00
parent 3fe7aee173
commit dbd07caaec
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
16 changed files with 459 additions and 314 deletions

View File

@ -74,6 +74,26 @@ fun <T> rememberPreferenceAsMutableState(
return state return state
} }
@Composable
fun <T> rememberPreferenceAsState(
key: Preferences.Key<T>,
defaultValue: T
): State<T> {
val dataStore = LocalContext.current.dataStore
val state = remember { mutableStateOf(defaultValue) }
LaunchedEffect(Unit) {
dataStore.data.map { it[key] ?: defaultValue }.distinctUntilChanged()
.collect { state.value = it }
}
LaunchedEffect(state.value) {
dataStore.edit { it[key] = state.value }
}
return state
}
@SuppressLint("FlowOperatorInvokedInComposition") @SuppressLint("FlowOperatorInvokedInComposition")
@Composable @Composable
fun <T> DataStore<Preferences>.collectPreferenceAsState( fun <T> DataStore<Preferences>.collectPreferenceAsState(

View File

@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
@ -279,30 +280,17 @@ class MainActivityV2 : BaseComposeActivity() {
LocalDevicePosture provides devicePostureFlow.collectAsState(), LocalDevicePosture provides devicePostureFlow.collectAsState(),
) { ) {
Box { Box {
if (ThemeUtil.isTranslucentTheme(ExtendedTheme.colors.theme)) { TranslucentThemeBackground()
val backgroundPath by rememberPreferenceAsMutableState(
key = stringPreferencesKey(
"translucent_theme_background_path"
),
defaultValue = ""
)
if (backgroundPath.isNotEmpty()) {
AsyncImage(
imageUri = newFileUri(backgroundPath),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
Surface( Surface(
color = ExtendedTheme.colors.background color = ExtendedTheme.colors.background
) { ) {
val animationSpec = spring( val animationSpec = remember {
stiffness = Spring.StiffnessMediumLow, spring(
visibilityThreshold = IntOffset.VisibilityThreshold stiffness = Spring.StiffnessMediumLow,
) visibilityThreshold = IntOffset.VisibilityThreshold
)
}
val engine = rememberAnimatedNavHostEngine( val engine = rememberAnimatedNavHostEngine(
navHostContentAlignment = Alignment.TopStart, navHostContentAlignment = Alignment.TopStart,
rootDefaultAnimations = RootNavGraphDefaultAnimations( rootDefaultAnimations = RootNavGraphDefaultAnimations(
@ -337,9 +325,6 @@ class MainActivityV2 : BaseComposeActivity() {
), ),
) )
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
onGlobalEvent<GlobalEvent.NavigateUp> {
navController.navigateUp()
}
val bottomSheetNavigator = val bottomSheetNavigator =
rememberBottomSheetNavigator( rememberBottomSheetNavigator(
animationSpec = spring(stiffness = Spring.StiffnessMediumLow), animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
@ -373,6 +358,23 @@ class MainActivityV2 : BaseComposeActivity() {
} }
} }
@Composable
private fun TranslucentThemeBackground() {
if (ThemeUtil.isTranslucentTheme(ExtendedTheme.colors.theme)) {
val backgroundPath by rememberPreferenceAsMutableState(
key = stringPreferencesKey("translucent_theme_background_path"),
defaultValue = ""
)
val backgroundUri by remember { derivedStateOf { newFileUri(backgroundPath) } }
AsyncImage(
imageUri = backgroundUri,
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
}
private inner class NewMessageReceiver : BroadcastReceiver() { private inner class NewMessageReceiver : BroadcastReceiver() {
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {

View File

@ -15,11 +15,13 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
sealed interface GlobalEvent { sealed interface GlobalEvent : UiEvent {
object AccountSwitched : GlobalEvent object AccountSwitched : GlobalEvent
object NavigateUp : GlobalEvent object NavigateUp : GlobalEvent
data class Refresh(val key: String) : GlobalEvent
data class StartSelectImages( data class StartSelectImages(
val id: String, val id: String,
val maxCount: Int, val maxCount: Int,
@ -40,20 +42,24 @@ sealed interface GlobalEvent {
) : GlobalEvent ) : GlobalEvent
} }
private val globalEventSharedFlow: MutableSharedFlow<GlobalEvent> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { private val globalEventSharedFlow: MutableSharedFlow<UiEvent> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST) MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
} }
val GlobalEventFlow = globalEventSharedFlow.asSharedFlow() val GlobalEventFlow = globalEventSharedFlow.asSharedFlow()
fun CoroutineScope.emitGlobalEvent(event: GlobalEvent) { fun CoroutineScope.emitGlobalEvent(event: UiEvent) {
launch { launch {
globalEventSharedFlow.emit(event) globalEventSharedFlow.emit(event)
} }
} }
suspend fun emitGlobalEvent(event: UiEvent) {
globalEventSharedFlow.emit(event)
}
@Composable @Composable
inline fun <reified Event : GlobalEvent> onGlobalEvent( inline fun <reified Event : UiEvent> onGlobalEvent(
coroutineScope: CoroutineScope = rememberCoroutineScope(), coroutineScope: CoroutineScope = rememberCoroutineScope(),
noinline filter: (Event) -> Boolean = { true }, noinline filter: (Event) -> Boolean = { true },
noinline listener: suspend (Event) -> Unit noinline listener: suspend (Event) -> Unit

View File

@ -92,6 +92,7 @@ import com.huanchengfly.tieba.post.activities.SearchPostActivity
import com.huanchengfly.tieba.post.api.models.protos.frsPage.ForumInfo import com.huanchengfly.tieba.post.api.models.protos.frsPage.ForumInfo
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.onEvent import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.dataStore import com.huanchengfly.tieba.post.dataStore
@ -99,6 +100,7 @@ import com.huanchengfly.tieba.post.getInt
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.models.database.History import com.huanchengfly.tieba.post.models.database.History
import com.huanchengfly.tieba.post.pxToDp import com.huanchengfly.tieba.post.pxToDp
import com.huanchengfly.tieba.post.toastShort
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
@ -132,7 +134,6 @@ import com.ramcosta.composedestinations.annotation.DeepLink
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
@ -468,13 +469,6 @@ fun ForumPage(
} }
} }
val eventFlows = remember {
listOf(
MutableSharedFlow<ForumThreadListUiEvent>(),
MutableSharedFlow<ForumThreadListUiEvent>()
)
}
val unlikeDialogState = rememberDialogState() val unlikeDialogState = rememberDialogState()
LaunchedEffect(forumInfo) { LaunchedEffect(forumInfo) {
@ -600,11 +594,14 @@ fun ForumPage(
when (context.appPreferences.forumFabFunction) { when (context.appPreferences.forumFabFunction) {
"refresh" -> { "refresh" -> {
coroutineScope.launch { coroutineScope.launch {
eventFlows[pagerState.currentPage].emit( emitGlobalEvent(
ForumThreadListUiEvent.BackToTop ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
)
) )
eventFlows[pagerState.currentPage].emit( emitGlobalEvent(
ForumThreadListUiEvent.Refresh( ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
getSortType( getSortType(
context, context,
forumName forumName
@ -616,14 +613,16 @@ fun ForumPage(
"back_to_top" -> { "back_to_top" -> {
coroutineScope.launch { coroutineScope.launch {
eventFlows[pagerState.currentPage].emit( emitGlobalEvent(
ForumThreadListUiEvent.BackToTop ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
)
) )
} }
} }
else -> { else -> {
context.toastShort(R.string.toast_feature_unavailable)
} }
} }
}, },
@ -752,8 +751,11 @@ fun ForumPage(
setSortType(context, forumName, value) setSortType(context, forumName, value)
} }
coroutineScope.launch { coroutineScope.launch {
eventFlows[pagerState.currentPage].emit( emitGlobalEvent(
ForumThreadListUiEvent.Refresh(value) ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
value
)
) )
} }
currentSortType = value currentSortType = value
@ -837,7 +839,6 @@ fun ForumPage(
ForumThreadListPage( ForumThreadListPage(
forumId = forumInfo!!.get { id }, forumId = forumInfo!!.get { id },
forumName = forumInfo!!.get { name }, forumName = forumInfo!!.get { name },
eventFlow = eventFlows[it],
isGood = it == 1, isGood = it == 1,
lazyListState = lazyListStates[it] lazyListState = lazyListStates[it]
) )

View File

@ -1,6 +1,7 @@
package com.huanchengfly.tieba.post.ui.page.forum.threadlist package com.huanchengfly.tieba.post.ui.page.forum.threadlist
import android.content.Context import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -45,6 +46,7 @@ import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindo
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.onEvent 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.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
@ -58,7 +60,9 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import kotlinx.coroutines.flow.Flow import com.huanchengfly.tieba.post.utils.appPreferences
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
private fun getFirstLoadIntent( private fun getFirstLoadIntent(
context: Context, context: Context,
@ -133,7 +137,7 @@ private fun GoodClassifyTabs(
@Composable @Composable
private fun ThreadList( private fun ThreadList(
state: LazyListState, state: LazyListState,
itemHoldersProvider: () -> List<ImmutableHolder<ThreadInfo>>, items: ImmutableList<ThreadItemData>,
isGood: Boolean, isGood: Boolean,
goodClassifyId: Int?, goodClassifyId: Int?,
goodClassifyHoldersProvider: () -> List<ImmutableHolder<Classify>>, goodClassifyHoldersProvider: () -> List<ImmutableHolder<Classify>>,
@ -142,7 +146,6 @@ private fun ThreadList(
onAgree: (ThreadInfo) -> Unit, onAgree: (ThreadInfo) -> Unit,
onClassifySelected: (Int) -> Unit onClassifySelected: (Int) -> Unit
) { ) {
val itemHolders = itemHoldersProvider()
val windowSizeClass = LocalWindowSizeClass.current val windowSizeClass = LocalWindowSizeClass.current
val itemFraction = when (windowSizeClass.widthSizeClass) { val itemFraction = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> 0.5f WindowWidthSizeClass.Expanded -> 0.5f
@ -164,12 +167,12 @@ private fun ThreadList(
} }
} }
itemsIndexed( itemsIndexed(
items = itemHolders, items = items,
key = { index, holder -> key = { index, (holder) ->
val (item) = holder val (item) = holder
"${index}_${item.id}" "${index}_${item.id}"
}, },
contentType = { _, holder -> contentType = { _, (holder) ->
val (item) = holder val (item) = holder
if (item.isTop == 1) ItemType.Top if (item.isTop == 1) ItemType.Top
else { else {
@ -180,7 +183,26 @@ private fun ThreadList(
else ItemType.PlainText else ItemType.PlainText
} }
} }
) { index, holder -> ) { index, (holder, blocked) ->
if (blocked) {
if (!LocalContext.current.appPreferences.hideBlockedContent) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
.clip(RoundedCornerShape(6.dp))
.background(ExtendedTheme.colors.floorCard)
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
Text(
text = stringResource(id = R.string.tip_blocked_thread),
style = MaterialTheme.typography.caption,
color = ExtendedTheme.colors.textSecondary
)
}
}
return@itemsIndexed
}
val (item) = holder val (item) = holder
Column( Column(
modifier = Modifier.fillMaxWidth(itemFraction) modifier = Modifier.fillMaxWidth(itemFraction)
@ -215,7 +237,7 @@ private fun ThreadList(
} }
} else { } else {
if (index > 0) { if (index > 0) {
if (itemHolders[index - 1].item.isTop == 1) { if (items[index - 1].thread.get { isTop } == 1) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp)) VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp))
@ -237,7 +259,6 @@ private fun ThreadList(
fun ForumThreadListPage( fun ForumThreadListPage(
forumId: Long, forumId: Long,
forumName: String, forumName: String,
eventFlow: Flow<ForumThreadListUiEvent>,
isGood: Boolean = false, isGood: Boolean = false,
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>() viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>()
@ -249,10 +270,10 @@ fun ForumThreadListPage(
viewModel.send(getFirstLoadIntent(context, forumName, isGood)) viewModel.send(getFirstLoadIntent(context, forumName, isGood))
viewModel.initialized = true viewModel.initialized = true
} }
eventFlow.onEvent<ForumThreadListUiEvent.Refresh> { onGlobalEvent<ForumThreadListUiEvent.Refresh> {
viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType)) viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType))
} }
eventFlow.onEvent<ForumThreadListUiEvent.BackToTop> { onGlobalEvent<ForumThreadListUiEvent.BackToTop> {
lazyListState.animateScrollToItem(0) lazyListState.animateScrollToItem(0)
} }
viewModel.onEvent<ForumThreadListUiEvent.AgreeFail> { viewModel.onEvent<ForumThreadListUiEvent.AgreeFail> {
@ -293,11 +314,11 @@ fun ForumThreadListPage(
) )
val threadList by viewModel.uiState.collectPartialAsState( val threadList by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::threadList, prop1 = ForumThreadListUiState::threadList,
initial = emptyList() initial = persistentListOf()
) )
val threadListIds by viewModel.uiState.collectPartialAsState( val threadListIds by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::threadListIds, prop1 = ForumThreadListUiState::threadListIds,
initial = emptyList() initial = persistentListOf()
) )
val goodClassifyId by viewModel.uiState.collectPartialAsState( val goodClassifyId by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::goodClassifyId, prop1 = ForumThreadListUiState::goodClassifyId,
@ -305,7 +326,7 @@ fun ForumThreadListPage(
) )
val goodClassifies by viewModel.uiState.collectPartialAsState( val goodClassifies by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::goodClassifies, prop1 = ForumThreadListUiState::goodClassifies,
initial = emptyList() initial = persistentListOf()
) )
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing, refreshing = isRefreshing,
@ -330,7 +351,7 @@ fun ForumThreadListPage(
) { ) {
ThreadList( ThreadList(
state = lazyListState, state = lazyListState,
itemHoldersProvider = { threadList }, items = threadList,
isGood = isGood, isGood = isGood,
goodClassifyId = goodClassifyId, goodClassifyId = goodClassifyId,
goodClassifyHoldersProvider = { goodClassifies }, goodClassifyHoldersProvider = { goodClassifies },

View File

@ -1,5 +1,6 @@
package com.huanchengfly.tieba.post.ui.page.forum.threadlist package com.huanchengfly.tieba.post.ui.page.forum.threadlist
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.AgreeBean import com.huanchengfly.tieba.post.api.models.AgreeBean
@ -20,7 +21,11 @@ import com.huanchengfly.tieba.post.arch.UiIntent
import com.huanchengfly.tieba.post.arch.UiState import com.huanchengfly.tieba.post.arch.UiState
import com.huanchengfly.tieba.post.arch.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.repository.FrsPageRepository import com.huanchengfly.tieba.post.repository.FrsPageRepository
import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
@ -59,6 +64,12 @@ enum class ForumThreadListType {
Latest, Good Latest, Good
} }
@Immutable
data class ThreadItemData(
val thread: ImmutableHolder<ThreadInfo>,
val blocked: Boolean = thread.get { shouldBlock() }
)
@Stable @Stable
@HiltViewModel @HiltViewModel
class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() { class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() {
@ -98,8 +109,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
) )
.map<FrsPageResponse, ForumThreadListPartialChange.FirstLoad> { response -> .map<FrsPageResponse, ForumThreadListPartialChange.FirstLoad> { response ->
if (response.data_?.page == null) throw TiebaUnknownException if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.FirstLoad.Success( ForumThreadListPartialChange.FirstLoad.Success(
response.data_.thread_list.wrapImmutable(), threadList,
response.data_.thread_id_list, response.data_.thread_id_list,
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(), (response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
goodClassifyId.takeIf { type == ForumThreadListType.Good }, goodClassifyId.takeIf { type == ForumThreadListType.Good },
@ -120,8 +133,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
) )
.map<FrsPageResponse, ForumThreadListPartialChange.Refresh> { response -> .map<FrsPageResponse, ForumThreadListPartialChange.Refresh> { response ->
if (response.data_?.page == null) throw TiebaUnknownException if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.Refresh.Success( ForumThreadListPartialChange.Refresh.Success(
response.data_.thread_list.wrapImmutable(), threadList,
response.data_.thread_id_list, response.data_.thread_id_list,
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(), (response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
goodClassifyId.takeIf { type == ForumThreadListType.Good }, goodClassifyId.takeIf { type == ForumThreadListType.Good },
@ -142,8 +157,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
threadListIds.subList(0, size).joinToString(separator = ",") { "$it" } threadListIds.subList(0, size).joinToString(separator = ",") { "$it" }
).map { response -> ).map { response ->
if (response.data_ == null) throw TiebaUnknownException if (response.data_ == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.LoadMore.Success( ForumThreadListPartialChange.LoadMore.Success(
threadList = response.data_.thread_list.wrapImmutable(), threadList = threadList,
threadListIds = threadListIds.drop(size), threadListIds = threadListIds.drop(size),
currentPage = currentPage, currentPage = currentPage,
hasMore = response.data_.thread_list.isNotEmpty() hasMore = response.data_.thread_list.isNotEmpty()
@ -159,8 +176,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
) )
.map<FrsPageResponse, ForumThreadListPartialChange.LoadMore> { response -> .map<FrsPageResponse, ForumThreadListPartialChange.LoadMore> { response ->
if (response.data_?.page == null) throw TiebaUnknownException if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.LoadMore.Success( ForumThreadListPartialChange.LoadMore.Success(
threadList = response.data_.thread_list.wrapImmutable(), threadList = threadList,
threadListIds = response.data_.thread_id_list, threadListIds = response.data_.thread_id_list,
currentPage = currentPage + 1, currentPage = currentPage + 1,
response.data_.page.has_more == 1 response.data_.page.has_more == 1
@ -233,9 +252,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState Start -> oldState
is Success -> oldState.copy( is Success -> oldState.copy(
isRefreshing = false, isRefreshing = false,
threadList = threadList, threadList = threadList.toImmutableList(),
threadListIds = threadListIds, threadListIds = threadListIds.toImmutableList(),
goodClassifies = goodClassifies, goodClassifies = goodClassifies.toImmutableList(),
goodClassifyId = goodClassifyId, goodClassifyId = goodClassifyId,
currentPage = 1, currentPage = 1,
hasMore = hasMore hasMore = hasMore
@ -247,7 +266,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : FirstLoad() object Start : FirstLoad()
data class Success( data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>, val threadList: List<ThreadItemData>,
val threadListIds: List<Long>, val threadListIds: List<Long>,
val goodClassifies: List<ImmutableHolder<Classify>>, val goodClassifies: List<ImmutableHolder<Classify>>,
val goodClassifyId: Int?, val goodClassifyId: Int?,
@ -265,9 +284,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState.copy(isRefreshing = true) Start -> oldState.copy(isRefreshing = true)
is Success -> oldState.copy( is Success -> oldState.copy(
isRefreshing = false, isRefreshing = false,
threadList = threadList, threadList = threadList.toImmutableList(),
threadListIds = threadListIds, threadListIds = threadListIds.toImmutableList(),
goodClassifies = goodClassifies, goodClassifies = goodClassifies.toImmutableList(),
goodClassifyId = goodClassifyId, goodClassifyId = goodClassifyId,
currentPage = 1, currentPage = 1,
hasMore = hasMore hasMore = hasMore
@ -279,7 +298,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : Refresh() object Start : Refresh()
data class Success( data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>, val threadList: List<ThreadItemData>,
val threadListIds: List<Long>, val threadListIds: List<Long>,
val goodClassifies: List<ImmutableHolder<Classify>>, val goodClassifies: List<ImmutableHolder<Classify>>,
val goodClassifyId: Int? = null, val goodClassifyId: Int? = null,
@ -297,8 +316,8 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState.copy(isLoadingMore = true) Start -> oldState.copy(isLoadingMore = true)
is Success -> oldState.copy( is Success -> oldState.copy(
isLoadingMore = false, isLoadingMore = false,
threadList = oldState.threadList + threadList, threadList = (oldState.threadList + threadList).toImmutableList(),
threadListIds = threadListIds, threadListIds = threadListIds.toImmutableList(),
currentPage = currentPage, currentPage = currentPage,
hasMore = hasMore hasMore = hasMore
) )
@ -309,7 +328,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : LoadMore() object Start : LoadMore()
data class Success( data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>, val threadList: List<ThreadItemData>,
val threadListIds: List<Long>, val threadListIds: List<Long>,
val currentPage: Int, val currentPage: Int,
val hasMore: Boolean, val hasMore: Boolean,
@ -321,16 +340,16 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
} }
sealed class Agree private constructor() : ForumThreadListPartialChange { sealed class Agree private constructor() : ForumThreadListPartialChange {
private fun List<ImmutableHolder<ThreadInfo>>.updateAgreeStatus( private fun List<ThreadItemData>.updateAgreeStatus(
threadId: Long, id: Long,
hasAgree: Int hasAgree: Int
): List<ImmutableHolder<ThreadInfo>> { ): ImmutableList<ThreadItemData> {
return map { holder -> return map { data ->
val (threadInfo) = holder val (thread) = data
if (threadInfo.threadId == threadId) { if (thread.get { id } == id) {
threadInfo.updateAgreeStatus(hasAgree) ThreadItemData(thread.getImmutable { updateAgreeStatus(hasAgree) })
} else threadInfo } else data
}.wrapImmutable() }.toImmutableList()
} }
override fun reduce(oldState: ForumThreadListUiState): ForumThreadListUiState = override fun reduce(oldState: ForumThreadListUiState): ForumThreadListUiState =
@ -386,9 +405,9 @@ data class ForumThreadListUiState(
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val isLoadingMore: Boolean = false, val isLoadingMore: Boolean = false,
val goodClassifyId: Int? = null, val goodClassifyId: Int? = null,
val threadList: List<ImmutableHolder<ThreadInfo>> = emptyList(), val threadList: ImmutableList<ThreadItemData> = persistentListOf(),
val threadListIds: List<Long> = emptyList(), val threadListIds: ImmutableList<Long> = persistentListOf(),
val goodClassifies: List<ImmutableHolder<Classify>> = emptyList(), val goodClassifies: ImmutableList<ImmutableHolder<Classify>> = persistentListOf(),
val currentPage: Int = 1, val currentPage: Int = 1,
val hasMore: Boolean = true, val hasMore: Boolean = true,
) : UiState ) : UiState
@ -403,8 +422,11 @@ sealed interface ForumThreadListUiEvent : UiEvent {
) : ForumThreadListUiEvent ) : ForumThreadListUiEvent
data class Refresh( data class Refresh(
val isGood: Boolean,
val sortType: Int val sortType: Int
) : ForumThreadListUiEvent ) : ForumThreadListUiEvent
object BackToTop : ForumThreadListUiEvent data class BackToTop(
val isGood: Boolean
) : ForumThreadListUiEvent
} }

View File

@ -18,9 +18,11 @@ import androidx.compose.material.icons.rounded.Inventory2
import androidx.compose.material.icons.rounded.Notifications import androidx.compose.material.icons.rounded.Notifications
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -28,12 +30,16 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource import androidx.compose.ui.res.vectorResource
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.huanchengfly.tieba.post.LocalDevicePosture import com.huanchengfly.tieba.post.LocalDevicePosture
import com.huanchengfly.tieba.post.LocalNotificationCountFlow import com.huanchengfly.tieba.post.LocalNotificationCountFlow
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.collectPartialAsState
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.rememberPreferenceAsState
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowHeightSizeClass import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowHeightSizeClass
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
@ -52,7 +58,6 @@ import com.ramcosta.composedestinations.annotation.RootNavGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@ -100,20 +105,16 @@ fun MainPage(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
viewModel: MainViewModel = pageViewModel<MainUiIntent, MainViewModel>(emptyList()), viewModel: MainViewModel = pageViewModel<MainUiIntent, MainViewModel>(emptyList()),
) { ) {
val windowSizeClass = LocalWindowSizeClass.current
val windowHeightSizeClass by rememberUpdatedState(newValue = windowSizeClass.heightSizeClass)
val windowWidthSizeClass by rememberUpdatedState(newValue = windowSizeClass.widthSizeClass)
val foldingDevicePosture by LocalDevicePosture.current
val messageCount by viewModel.uiState.collectPartialAsState( val messageCount by viewModel.uiState.collectPartialAsState(
prop1 = MainUiState::messageCount, prop1 = MainUiState::messageCount,
initial = 0 initial = 0
) )
val eventFlows = remember {
listOf(
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
)
}
val notificationCountFlow = LocalNotificationCountFlow.current val notificationCountFlow = LocalNotificationCountFlow.current
LaunchedEffect(null) { LaunchedEffect(null) {
notificationCountFlow.collect { notificationCountFlow.collect {
@ -121,104 +122,118 @@ fun MainPage(
} }
} }
val hideExplore by rememberPreferenceAsState(
key = booleanPreferencesKey("hideExplore"),
defaultValue = false
)
val pagerState = rememberPagerState() val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val themeColors = ExtendedTheme.colors val themeColors = ExtendedTheme.colors
val windowSizeClass = LocalWindowSizeClass.current val navigationItems = remember(messageCount) {
val foldingDevicePosture by LocalDevicePosture.current listOfNotNull(
val navigationItems = listOfNotNull( NavigationItem(
NavigationItem( id = "home",
id = "home", icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 },
icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 }, title = { stringResource(id = R.string.title_main) },
title = { stringResource(id = R.string.title_main) }, content = {
content = { HomePage(
HomePage( canOpenExplore = !LocalContext.current.appPreferences.hideExplore
eventFlow = eventFlows[0], ) {
canOpenExplore = !LocalContext.current.appPreferences.hideExplore coroutineScope.launch {
) { pagerState.scrollToPage(1)
coroutineScope.launch { }
pagerState.scrollToPage(1)
} }
} }
} ),
), if (hideExplore) null
if (LocalContext.current.appPreferences.hideExplore) null else NavigationItem(
else NavigationItem( id = "explore",
id = "explore", icon = {
icon = { if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys)
if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys) else ImageVector.vectorResource( else ImageVector.vectorResource(id = R.drawable.ic_outline_toys)
id = R.drawable.ic_outline_toys },
) title = { stringResource(id = R.string.title_explore) },
}, content = {
title = { stringResource(id = R.string.title_explore) }, ExplorePage()
content = { }
ExplorePage(eventFlows[1]) ),
} NavigationItem(
), id = "notification",
NavigationItem( icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications },
id = "notification", title = { stringResource(id = R.string.title_notifications) },
icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications }, badge = messageCount > 0,
title = { stringResource(id = R.string.title_notifications) }, badgeText = "$messageCount",
badge = messageCount > 0, onClick = {
badgeText = "$messageCount", viewModel.send(MainUiIntent.NewMessage.Clear)
onClick = { },
viewModel.send(MainUiIntent.NewMessage.Clear) content = {
}, NotificationsPage()
content = { }
NotificationsPage() ),
} NavigationItem(
), id = "user",
NavigationItem( icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle },
id = "user", title = { stringResource(id = R.string.title_user) },
icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle }, content = {
title = { stringResource(id = R.string.title_user) }, UserPage()
content = { }
UserPage() ),
} ).toImmutableList()
), }
).toImmutableList()
val navigationType = when (windowSizeClass.widthSizeClass) { val navigationType by remember {
WindowWidthSizeClass.Compact -> { derivedStateOf {
MainNavigationType.BOTTOM_NAVIGATION when (windowWidthSizeClass) {
} WindowWidthSizeClass.Compact -> {
WindowWidthSizeClass.Medium -> { MainNavigationType.BOTTOM_NAVIGATION
MainNavigationType.NAVIGATION_RAIL }
}
WindowWidthSizeClass.Expanded -> { WindowWidthSizeClass.Medium -> {
if (foldingDevicePosture is DevicePosture.BookPosture) { MainNavigationType.NAVIGATION_RAIL
MainNavigationType.NAVIGATION_RAIL }
} else {
MainNavigationType.PERMANENT_NAVIGATION_DRAWER WindowWidthSizeClass.Expanded -> {
if (foldingDevicePosture is DevicePosture.BookPosture) {
MainNavigationType.NAVIGATION_RAIL
} else {
MainNavigationType.PERMANENT_NAVIGATION_DRAWER
}
}
else -> {
MainNavigationType.BOTTOM_NAVIGATION
}
} }
} }
else -> {
MainNavigationType.BOTTOM_NAVIGATION
}
} }
/** /**
* Content inside Navigation Rail/Drawer can also be positioned at top, bottom or center for * Content inside Navigation Rail/Drawer can also be positioned at top, bottom or center for
* ergonomics and reachability depending upon the height of the device. * ergonomics and reachability depending upon the height of the device.
*/ */
val navigationContentPosition = when (windowSizeClass.heightSizeClass) { val navigationContentPosition by remember {
WindowHeightSizeClass.Compact -> { derivedStateOf {
MainNavigationContentPosition.TOP when (windowHeightSizeClass) {
} WindowHeightSizeClass.Compact -> {
MainNavigationContentPosition.TOP
}
WindowHeightSizeClass.Medium, WindowHeightSizeClass.Medium,
WindowHeightSizeClass.Expanded -> { WindowHeightSizeClass.Expanded -> {
MainNavigationContentPosition.CENTER MainNavigationContentPosition.CENTER
} }
else -> { else -> {
MainNavigationContentPosition.TOP MainNavigationContentPosition.TOP
}
}
} }
} }
val onReselected: (Int) -> Unit = { val onReselected: (Int) -> Unit = {
coroutineScope.launch { coroutineScope.emitGlobalEvent(
eventFlows[it].emit(MainUiEvent.Refresh) GlobalEvent.Refresh(navigationItems[it].id)
} )
} }
NavigationWrapper( NavigationWrapper(
currentPosition = pagerState.currentPage, currentPosition = pagerState.currentPage,

View File

@ -31,6 +31,7 @@ import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -414,6 +415,7 @@ fun BottomNavigation(
} }
} }
@Immutable
data class NavigationItem( data class NavigationItem(
val id: String, val id: String,
val icon: @Composable (selected: Boolean) -> ImageVector, val icon: @Composable (selected: Boolean) -> ImageVector,

View File

@ -1,8 +1,10 @@
package com.huanchengfly.tieba.post.ui.page.main.explore package com.huanchengfly.tieba.post.ui.page.main.explore
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Scaffold import androidx.compose.material.Scaffold
import androidx.compose.material.Tab import androidx.compose.material.Tab
@ -11,6 +13,7 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -21,10 +24,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.NewSearchActivity import com.huanchengfly.tieba.post.activities.NewSearchActivity
import com.huanchengfly.tieba.post.arch.onEvent import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.page.main.explore.concern.ConcernPage import com.huanchengfly.tieba.post.ui.page.main.explore.concern.ConcernPage
import com.huanchengfly.tieba.post.ui.page.main.explore.hot.HotPage import com.huanchengfly.tieba.post.ui.page.main.explore.hot.HotPage
import com.huanchengfly.tieba.post.ui.page.main.explore.personalized.PersonalizedPage import com.huanchengfly.tieba.post.ui.page.main.explore.personalized.PersonalizedPage
@ -34,45 +38,93 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import kotlinx.coroutines.flow.Flow import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Immutable
data class ExplorePageItem(
val id: String,
val name: @Composable () -> Unit,
val content: @Composable () -> Unit,
)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ExplorePage( private fun ColumnScope.ExplorePageTab(
eventFlow: Flow<MainUiEvent>, pagerState: PagerState,
pages: ImmutableList<ExplorePageItem>
) { ) {
val coroutineScope = rememberCoroutineScope()
TabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
PagerTabIndicator(
pagerState = pagerState,
tabPositions = tabPositions
)
},
divider = {},
backgroundColor = Color.Transparent,
contentColor = ExtendedTheme.colors.onTopBar,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(76.dp * pages.size),
) {
pages.forEachIndexed { index, item ->
Tab(
text = item.name,
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
if (pagerState.currentPage == index) {
coroutineScope.emitGlobalEvent(GlobalEvent.Refresh(item.id))
} else {
pagerState.animateScrollToPage(index)
}
}
},
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExplorePage() {
val account = LocalAccount.current val account = LocalAccount.current
val context = LocalContext.current val context = LocalContext.current
val eventFlows = remember { val loggedIn = remember(account) { account != null }
listOf(
MutableSharedFlow<MainUiEvent>(), val pages = remember {
MutableSharedFlow<MainUiEvent>(), listOfNotNull(
MutableSharedFlow<MainUiEvent>() if (loggedIn) ExplorePageItem(
) "concern",
{ Text(text = stringResource(id = R.string.title_concern)) },
{ ConcernPage() }
) else null,
ExplorePageItem(
"personalized",
{ Text(text = stringResource(id = R.string.title_personalized)) },
{ PersonalizedPage() }
),
ExplorePageItem(
"hot",
{ Text(text = stringResource(id = R.string.title_hot)) },
{ HotPage() }
),
).toImmutableList()
} }
val firstIndex = if (account != null) 0 else -1
val pages = listOfNotNull<Pair<String, (@Composable () -> Unit)>>(
if (account != null) stringResource(id = R.string.title_concern) to @Composable {
ConcernPage(eventFlows[firstIndex + 0])
} else null,
stringResource(id = R.string.title_personalized) to @Composable {
PersonalizedPage(eventFlows[firstIndex + 1])
},
stringResource(id = R.string.title_hot) to @Composable {
HotPage(eventFlows[firstIndex + 2])
},
)
val pagerState = rememberPagerState(initialPage = if (account != null) 1 else 0) val pagerState = rememberPagerState(initialPage = if (account != null) 1 else 0)
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
eventFlow.onEvent<MainUiEvent.Refresh> { onGlobalEvent<GlobalEvent.Refresh>(
eventFlows[pagerState.currentPage].emit(it) filter = { it.key == "explore" }
) {
coroutineScope.emitGlobalEvent(GlobalEvent.Refresh(pages[pagerState.currentPage].id))
} }
Scaffold( Scaffold(
@ -90,37 +142,7 @@ fun ExplorePage(
} }
}, },
) { ) {
TabRow( ExplorePageTab(pagerState = pagerState, pages = pages)
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
PagerTabIndicator(
pagerState = pagerState,
tabPositions = tabPositions
)
},
divider = {},
backgroundColor = Color.Transparent,
contentColor = ExtendedTheme.colors.onTopBar,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.width(76.dp * pages.size),
) {
pages.forEachIndexed { index, pair ->
Tab(
text = { Text(text = pair.first) },
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
if (pagerState.currentPage == index) {
eventFlows[pagerState.currentPage].emit(MainUiEvent.Refresh)
} else {
pagerState.animateScrollToPage(index)
}
}
},
)
}
}
} }
}, },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -129,12 +151,12 @@ fun ExplorePage(
contentPadding = paddingValues, contentPadding = paddingValues,
pageCount = pages.size, pageCount = pages.size,
state = pagerState, state = pagerState,
key = { pages[it].first }, key = { pages[it].id },
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
userScrollEnabled = true, userScrollEnabled = true,
) { ) {
pages[it].second() pages[it].content()
} }
} }
} }

View File

@ -1,6 +1,5 @@
package com.huanchengfly.tieba.post.ui.page.main.explore.concern package com.huanchengfly.tieba.post.ui.page.main.explore.concern
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -20,8 +19,9 @@ import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.api.models.protos.hasAgree import com.huanchengfly.tieba.post.api.models.protos.hasAgree
import com.huanchengfly.tieba.post.arch.BaseComposeActivity import com.huanchengfly.tieba.post.arch.BaseComposeActivity
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.collectPartialAsState 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.arch.pageViewModel
import com.huanchengfly.tieba.post.arch.wrapImmutable import com.huanchengfly.tieba.post.arch.wrapImmutable
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -29,17 +29,14 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun ConcernPage( fun ConcernPage(
eventFlow: Flow<MainUiEvent>,
viewModel: ConcernViewModel = pageViewModel() viewModel: ConcernViewModel = pageViewModel()
) { ) {
LazyLoad(loaded = viewModel.initialized) { LazyLoad(loaded = viewModel.initialized) {
@ -67,7 +64,9 @@ fun ConcernPage(
refreshing = isRefreshing, refreshing = isRefreshing,
onRefresh = { viewModel.send(ConcernUiIntent.Refresh) }) onRefresh = { viewModel.send(ConcernUiIntent.Refresh) })
eventFlow.onEvent<MainUiEvent.Refresh> { onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "concern" }
) {
viewModel.send(ConcernUiIntent.Refresh) viewModel.send(ConcernUiIntent.Refresh)
} }

View File

@ -2,7 +2,6 @@ package com.huanchengfly.tieba.post.ui.page.main.explore.hot
import android.graphics.Typeface import android.graphics.Typeface
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -48,9 +47,10 @@ import com.google.accompanist.placeholder.material.placeholder
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
import com.huanchengfly.tieba.post.api.models.protos.hasAgree import com.huanchengfly.tieba.post.api.models.protos.hasAgree
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.arch.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState 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.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.OrangeA700 import com.huanchengfly.tieba.post.ui.common.theme.compose.OrangeA700
@ -61,7 +61,6 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
@ -72,20 +71,20 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.items
import com.huanchengfly.tieba.post.ui.widgets.compose.itemsIndexed import com.huanchengfly.tieba.post.ui.widgets.compose.itemsIndexed
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Destination @Destination
@Composable @Composable
fun HotPage( fun HotPage(
eventFlow: Flow<MainUiEvent>,
viewModel: HotViewModel = pageViewModel() viewModel: HotViewModel = pageViewModel()
) { ) {
LazyLoad(loaded = viewModel.initialized) { LazyLoad(loaded = viewModel.initialized) {
viewModel.send(HotUiIntent.Load) viewModel.send(HotUiIntent.Load)
viewModel.initialized = true viewModel.initialized = true
} }
eventFlow.onEvent<MainUiEvent.Refresh> { onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "hot" }
) {
viewModel.send(HotUiIntent.Load) viewModel.send(HotUiIntent.Load)
} }
val navigator = LocalNavigator.current val navigator = LocalNavigator.current

View File

@ -50,9 +50,10 @@ 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.api.models.protos.personalized.ThreadPersonalized
import com.huanchengfly.tieba.post.arch.BaseComposeActivity import com.huanchengfly.tieba.post.arch.BaseComposeActivity
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent 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.ImmutableHolder
import com.huanchengfly.tieba.post.arch.collectPartialAsState 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.arch.pageViewModel
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
@ -60,19 +61,16 @@ import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClas
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun PersonalizedPage( fun PersonalizedPage(
eventFlow: Flow<MainUiEvent>,
viewModel: PersonalizedViewModel = pageViewModel() viewModel: PersonalizedViewModel = pageViewModel()
) { ) {
LazyLoad(loaded = viewModel.initialized) { LazyLoad(loaded = viewModel.initialized) {
@ -121,13 +119,11 @@ fun PersonalizedPage(
mutableStateOf(false) mutableStateOf(false)
} }
eventFlow.onEvent<MainUiEvent.Refresh> { onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "personalized" }
) {
viewModel.send(PersonalizedUiIntent.Refresh) viewModel.send(PersonalizedUiIntent.Refresh)
} }
viewModel.onEvent<PersonalizedUiEvent.RefreshSuccess> {
refreshCount = it.count
showRefreshTip = true
}
if (showRefreshTip) { if (showRefreshTip) {
LaunchedEffect(Unit) { LaunchedEffect(Unit) {

View File

@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 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.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -39,7 +40,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -69,15 +69,15 @@ import com.google.accompanist.placeholder.placeholder
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.activities.LoginActivity import com.huanchengfly.tieba.post.activities.LoginActivity
import com.huanchengfly.tieba.post.activities.NewSearchActivity import com.huanchengfly.tieba.post.activities.NewSearchActivity
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.collectPartialAsState 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.arch.pageViewModel
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator import com.huanchengfly.tieba.post.ui.page.LocalNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
import com.huanchengfly.tieba.post.ui.widgets.Chip import com.huanchengfly.tieba.post.ui.widgets.Chip
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
@ -96,7 +96,6 @@ import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import com.huanchengfly.tieba.post.utils.ImageUtil import com.huanchengfly.tieba.post.utils.ImageUtil
import com.huanchengfly.tieba.post.utils.TiebaUtil import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.huanchengfly.tieba.post.utils.appPreferences import com.huanchengfly.tieba.post.utils.appPreferences
import kotlinx.coroutines.flow.Flow
private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells { private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells {
return if (listSingle) { return if (listSingle) {
@ -232,47 +231,22 @@ private fun ForumItemPlaceholder(
@Composable @Composable
private fun ForumItem( private fun ForumItem(
viewModel: HomeViewModel,
item: HomeUiState.Forum, item: HomeUiState.Forum,
showAvatar: Boolean, showAvatar: Boolean,
isTopForum: Boolean = false onClick: (HomeUiState.Forum) -> Unit,
onUnfollow: (HomeUiState.Forum) -> Unit,
onAddTopForum: (HomeUiState.Forum) -> Unit,
onDeleteTopForum: (HomeUiState.Forum) -> Unit,
isTopForum: Boolean = false,
) { ) {
val navigator = LocalNavigator.current
val context = LocalContext.current val context = LocalContext.current
val menuState = rememberMenuState() val menuState = rememberMenuState()
var willUnfollow by remember {
mutableStateOf(false)
}
if (willUnfollow) {
val dialogState = rememberDialogState()
ConfirmDialog(
dialogState = dialogState,
onConfirm = { viewModel.send(HomeUiIntent.Unfollow(item.forumId, item.forumName)) },
modifier = Modifier,
onDismiss = {
willUnfollow = false
},
title = {
Text(
text = stringResource(
id = R.string.title_dialog_unfollow_forum,
item.forumName
)
)
}
)
LaunchedEffect(key1 = "launchUnfollowDialog") {
dialogState.show = true
}
}
LongClickMenu( LongClickMenu(
menuContent = { menuContent = {
if (isTopForum) { if (isTopForum) {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
viewModel.send(HomeUiIntent.TopForums.Delete(item.forumId)) onDeleteTopForum(item)
menuState.expanded = false menuState.expanded = false
} }
) { ) {
@ -281,7 +255,7 @@ private fun ForumItem(
} else { } else {
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
viewModel.send(HomeUiIntent.TopForums.Add(item)) onAddTopForum(item)
menuState.expanded = false menuState.expanded = false
} }
) { ) {
@ -298,7 +272,7 @@ private fun ForumItem(
} }
DropdownMenuItem( DropdownMenuItem(
onClick = { onClick = {
willUnfollow = true onUnfollow(item)
menuState.expanded = false menuState.expanded = false
} }
) { ) {
@ -307,7 +281,7 @@ private fun ForumItem(
}, },
menuState = menuState, menuState = menuState,
onClick = { onClick = {
navigator.navigate(ForumPageDestination(item.forumName)) onClick(item)
} }
) { ) {
Row( Row(
@ -374,7 +348,6 @@ private fun ForumItem(
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun HomePage( fun HomePage(
eventFlow: Flow<MainUiEvent>,
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>( viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
listOf( listOf(
HomeUiIntent.Refresh HomeUiIntent.Refresh
@ -385,6 +358,7 @@ fun HomePage(
) { ) {
val account = LocalAccount.current val account = LocalAccount.current
val context = LocalContext.current val context = LocalContext.current
val navigator = LocalNavigator.current
val isLoading by viewModel.uiState.collectPartialAsState( val isLoading by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::isLoading, prop1 = HomeUiState::isLoading,
initial = true initial = true
@ -401,14 +375,37 @@ fun HomePage(
prop1 = HomeUiState::error, prop1 = HomeUiState::error,
initial = null initial = null
) )
val isError by remember { derivedStateOf { error != null } } val isLoggedIn = remember(account) { account != null }
val isEmpty by remember { derivedStateOf { forums.isEmpty() } }
val hasTopForum by remember { derivedStateOf { topForums.isNotEmpty() } }
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) } var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
val isError by remember { derivedStateOf { error != null } }
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } } val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
eventFlow.onEvent<MainUiEvent.Refresh> { onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "home" }
) {
viewModel.send(HomeUiIntent.Refresh) viewModel.send(HomeUiIntent.Refresh)
} }
var unfollowForum by remember { mutableStateOf<HomeUiState.Forum?>(null) }
val confirmUnfollowDialog = rememberDialogState()
ConfirmDialog(
dialogState = confirmUnfollowDialog,
onConfirm = {
unfollowForum?.let {
viewModel.send(HomeUiIntent.Unfollow(it.forumId, it.forumName))
}
},
) {
Text(
text = stringResource(
id = R.string.title_dialog_unfollow_forum,
unfollowForum?.forumName.orEmpty()
)
)
}
Scaffold( Scaffold(
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
topBar = { topBar = {
@ -435,7 +432,7 @@ fun HomePage(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { contentPaddings -> ) { contentPaddings ->
StateScreen( StateScreen(
isEmpty = forums.isEmpty(), isEmpty = isEmpty,
isError = isError, isError = isError,
isLoading = isLoading, isLoading = isLoading,
modifier = Modifier.padding(contentPaddings), modifier = Modifier.padding(contentPaddings),
@ -444,7 +441,7 @@ fun HomePage(
}, },
emptyScreen = { emptyScreen = {
EmptyScreen( EmptyScreen(
loggedIn = account != null, loggedIn = isLoggedIn,
canOpenExplore = canOpenExplore, canOpenExplore = canOpenExplore,
onOpenExplore = onOpenExplore onOpenExplore = onOpenExplore
) )
@ -458,7 +455,8 @@ fun HomePage(
) { ) {
val pullRefreshState = rememberPullRefreshState( val pullRefreshState = rememberPullRefreshState(
refreshing = isLoading, refreshing = isLoading,
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }) onRefresh = { viewModel.send(HomeUiIntent.Refresh) }
)
Box( Box(
modifier = Modifier modifier = Modifier
.pullRefresh(pullRefreshState) .pullRefresh(pullRefreshState)
@ -468,15 +466,14 @@ fun HomePage(
state = gridState, state = gridState,
columns = gridCells, columns = gridCells,
contentPadding = PaddingValues(bottom = 12.dp), contentPadding = PaddingValues(bottom = 12.dp),
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(),
) { ) {
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) { item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) { SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
context.goToActivity<NewSearchActivity>() context.goToActivity<NewSearchActivity>()
} }
} }
if (topForums.isNotEmpty()) { if (hasTopForum) {
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) { item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column { Column {
Header( Header(
@ -486,9 +483,28 @@ fun HomePage(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
items(count = topForums.size, key = { "Top${topForums[it].forumId}" }) { items(
val item = topForums[it] items = topForums,
ForumItem(viewModel, item, listSingle, true) 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
)
} }
item( item(
key = "Spacer", key = "Spacer",
@ -506,9 +522,27 @@ fun HomePage(
} }
} }
} }
items(count = forums.size, key = { forums[it].forumId }) { items(
val item = forums[it] items = forums,
ForumItem(viewModel, item, listSingle) 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))
}
)
} }
} }

View File

@ -1,5 +1,7 @@
package com.huanchengfly.tieba.post.ui.page.main.home package com.huanchengfly.tieba.post.ui.page.main.home
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.CommonResponse import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse
@ -12,6 +14,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.litepal.LitePal import org.litepal.LitePal
@Stable
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() { class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
override fun createInitialState(): HomeUiState = HomeUiState() override fun createInitialState(): HomeUiState = HomeUiState()
@ -184,12 +187,14 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
} }
} }
@Immutable
data class HomeUiState( data class HomeUiState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val forums: List<Forum> = emptyList(), val forums: List<Forum> = emptyList(),
val topForums: List<Forum> = emptyList(), val topForums: List<Forum> = emptyList(),
val error: Throwable? = null, val error: Throwable? = null,
) : UiState { ) : UiState {
@Immutable
data class Forum( data class Forum(
val avatar: String, val avatar: String,
val forumId: String, val forumId: String,

View File

@ -109,7 +109,7 @@ fun ClickMenu(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LongClickMenu( fun LongClickMenu(
menuContent: @Composable() (ColumnScope.() -> Unit), menuContent: @Composable (ColumnScope.() -> Unit),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
menuState: MenuState = rememberMenuState(), menuState: MenuState = rememberMenuState(),
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,

View File

@ -706,4 +706,5 @@
<string name="above_is_latest_post">以上为最新回复</string> <string name="above_is_latest_post">以上为最新回复</string>
<string name="below_is_latest_post">以下为最新回复</string> <string name="below_is_latest_post">以下为最新回复</string>
<string name="btn_open_origin_thread">查看原贴</string> <string name="btn_open_origin_thread">查看原贴</string>
<string name="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string>
</resources> </resources>