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
}
@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")
@Composable
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.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
@ -279,30 +280,17 @@ class MainActivityV2 : BaseComposeActivity() {
LocalDevicePosture provides devicePostureFlow.collectAsState(),
) {
Box {
if (ThemeUtil.isTranslucentTheme(ExtendedTheme.colors.theme)) {
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
)
}
}
TranslucentThemeBackground()
Surface(
color = ExtendedTheme.colors.background
) {
val animationSpec = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
val animationSpec = remember {
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
)
}
val engine = rememberAnimatedNavHostEngine(
navHostContentAlignment = Alignment.TopStart,
rootDefaultAnimations = RootNavGraphDefaultAnimations(
@ -337,9 +325,6 @@ class MainActivityV2 : BaseComposeActivity() {
),
)
val navController = rememberAnimatedNavController()
onGlobalEvent<GlobalEvent.NavigateUp> {
navController.navigateUp()
}
val bottomSheetNavigator =
rememberBottomSheetNavigator(
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() {
@SuppressLint("RestrictedApi")
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.launch
sealed interface GlobalEvent {
sealed interface GlobalEvent : UiEvent {
object AccountSwitched : GlobalEvent
object NavigateUp : GlobalEvent
data class Refresh(val key: String) : GlobalEvent
data class StartSelectImages(
val id: String,
val maxCount: Int,
@ -40,20 +42,24 @@ sealed interface 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)
}
val GlobalEventFlow = globalEventSharedFlow.asSharedFlow()
fun CoroutineScope.emitGlobalEvent(event: GlobalEvent) {
fun CoroutineScope.emitGlobalEvent(event: UiEvent) {
launch {
globalEventSharedFlow.emit(event)
}
}
suspend fun emitGlobalEvent(event: UiEvent) {
globalEventSharedFlow.emit(event)
}
@Composable
inline fun <reified Event : GlobalEvent> onGlobalEvent(
inline fun <reified Event : UiEvent> onGlobalEvent(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
noinline filter: (Event) -> Boolean = { true },
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.arch.ImmutableHolder
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.pageViewModel
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.models.database.History
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.page.LocalNavigator
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.navigation.DestinationsNavigator
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
@ -468,13 +469,6 @@ fun ForumPage(
}
}
val eventFlows = remember {
listOf(
MutableSharedFlow<ForumThreadListUiEvent>(),
MutableSharedFlow<ForumThreadListUiEvent>()
)
}
val unlikeDialogState = rememberDialogState()
LaunchedEffect(forumInfo) {
@ -600,11 +594,14 @@ fun ForumPage(
when (context.appPreferences.forumFabFunction) {
"refresh" -> {
coroutineScope.launch {
eventFlows[pagerState.currentPage].emit(
ForumThreadListUiEvent.BackToTop
emitGlobalEvent(
ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
)
)
eventFlows[pagerState.currentPage].emit(
emitGlobalEvent(
ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
getSortType(
context,
forumName
@ -616,14 +613,16 @@ fun ForumPage(
"back_to_top" -> {
coroutineScope.launch {
eventFlows[pagerState.currentPage].emit(
ForumThreadListUiEvent.BackToTop
emitGlobalEvent(
ForumThreadListUiEvent.BackToTop(
pagerState.currentPage == 1
)
)
}
}
else -> {
context.toastShort(R.string.toast_feature_unavailable)
}
}
},
@ -752,8 +751,11 @@ fun ForumPage(
setSortType(context, forumName, value)
}
coroutineScope.launch {
eventFlows[pagerState.currentPage].emit(
ForumThreadListUiEvent.Refresh(value)
emitGlobalEvent(
ForumThreadListUiEvent.Refresh(
pagerState.currentPage == 1,
value
)
)
}
currentSortType = value
@ -837,7 +839,6 @@ fun ForumPage(
ForumThreadListPage(
forumId = forumInfo!!.get { id },
forumName = forumInfo!!.get { name },
eventFlow = eventFlows[it],
isGood = it == 1,
lazyListState = lazyListStates[it]
)

View File

@ -1,6 +1,7 @@
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
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.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
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.LocalSnackbarHostState
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(
context: Context,
@ -133,7 +137,7 @@ private fun GoodClassifyTabs(
@Composable
private fun ThreadList(
state: LazyListState,
itemHoldersProvider: () -> List<ImmutableHolder<ThreadInfo>>,
items: ImmutableList<ThreadItemData>,
isGood: Boolean,
goodClassifyId: Int?,
goodClassifyHoldersProvider: () -> List<ImmutableHolder<Classify>>,
@ -142,7 +146,6 @@ private fun ThreadList(
onAgree: (ThreadInfo) -> Unit,
onClassifySelected: (Int) -> Unit
) {
val itemHolders = itemHoldersProvider()
val windowSizeClass = LocalWindowSizeClass.current
val itemFraction = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Expanded -> 0.5f
@ -164,12 +167,12 @@ private fun ThreadList(
}
}
itemsIndexed(
items = itemHolders,
key = { index, holder ->
items = items,
key = { index, (holder) ->
val (item) = holder
"${index}_${item.id}"
},
contentType = { _, holder ->
contentType = { _, (holder) ->
val (item) = holder
if (item.isTop == 1) ItemType.Top
else {
@ -180,7 +183,26 @@ private fun ThreadList(
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
Column(
modifier = Modifier.fillMaxWidth(itemFraction)
@ -215,7 +237,7 @@ private fun ThreadList(
}
} else {
if (index > 0) {
if (itemHolders[index - 1].item.isTop == 1) {
if (items[index - 1].thread.get { isTop } == 1) {
Spacer(modifier = Modifier.height(8.dp))
}
VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp))
@ -237,7 +259,6 @@ private fun ThreadList(
fun ForumThreadListPage(
forumId: Long,
forumName: String,
eventFlow: Flow<ForumThreadListUiEvent>,
isGood: Boolean = false,
lazyListState: LazyListState = rememberLazyListState(),
viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>()
@ -249,10 +270,10 @@ fun ForumThreadListPage(
viewModel.send(getFirstLoadIntent(context, forumName, isGood))
viewModel.initialized = true
}
eventFlow.onEvent<ForumThreadListUiEvent.Refresh> {
onGlobalEvent<ForumThreadListUiEvent.Refresh> {
viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType))
}
eventFlow.onEvent<ForumThreadListUiEvent.BackToTop> {
onGlobalEvent<ForumThreadListUiEvent.BackToTop> {
lazyListState.animateScrollToItem(0)
}
viewModel.onEvent<ForumThreadListUiEvent.AgreeFail> {
@ -293,11 +314,11 @@ fun ForumThreadListPage(
)
val threadList by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::threadList,
initial = emptyList()
initial = persistentListOf()
)
val threadListIds by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::threadListIds,
initial = emptyList()
initial = persistentListOf()
)
val goodClassifyId by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::goodClassifyId,
@ -305,7 +326,7 @@ fun ForumThreadListPage(
)
val goodClassifies by viewModel.uiState.collectPartialAsState(
prop1 = ForumThreadListUiState::goodClassifies,
initial = emptyList()
initial = persistentListOf()
)
val pullRefreshState = rememberPullRefreshState(
refreshing = isRefreshing,
@ -330,7 +351,7 @@ fun ForumThreadListPage(
) {
ThreadList(
state = lazyListState,
itemHoldersProvider = { threadList },
items = threadList,
isGood = isGood,
goodClassifyId = goodClassifyId,
goodClassifyHoldersProvider = { goodClassifies },

View File

@ -1,5 +1,6 @@
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi
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.wrapImmutable
import com.huanchengfly.tieba.post.repository.FrsPageRepository
import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock
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.flow.Flow
import kotlinx.coroutines.flow.catch
@ -59,6 +64,12 @@ enum class ForumThreadListType {
Latest, Good
}
@Immutable
data class ThreadItemData(
val thread: ImmutableHolder<ThreadInfo>,
val blocked: Boolean = thread.get { shouldBlock() }
)
@Stable
@HiltViewModel
class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() {
@ -98,8 +109,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
)
.map<FrsPageResponse, ForumThreadListPartialChange.FirstLoad> { response ->
if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.FirstLoad.Success(
response.data_.thread_list.wrapImmutable(),
threadList,
response.data_.thread_id_list,
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
goodClassifyId.takeIf { type == ForumThreadListType.Good },
@ -120,8 +133,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
)
.map<FrsPageResponse, ForumThreadListPartialChange.Refresh> { response ->
if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.Refresh.Success(
response.data_.thread_list.wrapImmutable(),
threadList,
response.data_.thread_id_list,
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
goodClassifyId.takeIf { type == ForumThreadListType.Good },
@ -142,8 +157,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
threadListIds.subList(0, size).joinToString(separator = ",") { "$it" }
).map { response ->
if (response.data_ == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.LoadMore.Success(
threadList = response.data_.thread_list.wrapImmutable(),
threadList = threadList,
threadListIds = threadListIds.drop(size),
currentPage = currentPage,
hasMore = response.data_.thread_list.isNotEmpty()
@ -159,8 +176,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
)
.map<FrsPageResponse, ForumThreadListPartialChange.LoadMore> { response ->
if (response.data_?.page == null) throw TiebaUnknownException
val threadList =
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
ForumThreadListPartialChange.LoadMore.Success(
threadList = response.data_.thread_list.wrapImmutable(),
threadList = threadList,
threadListIds = response.data_.thread_id_list,
currentPage = currentPage + 1,
response.data_.page.has_more == 1
@ -233,9 +252,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState
is Success -> oldState.copy(
isRefreshing = false,
threadList = threadList,
threadListIds = threadListIds,
goodClassifies = goodClassifies,
threadList = threadList.toImmutableList(),
threadListIds = threadListIds.toImmutableList(),
goodClassifies = goodClassifies.toImmutableList(),
goodClassifyId = goodClassifyId,
currentPage = 1,
hasMore = hasMore
@ -247,7 +266,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : FirstLoad()
data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>,
val threadList: List<ThreadItemData>,
val threadListIds: List<Long>,
val goodClassifies: List<ImmutableHolder<Classify>>,
val goodClassifyId: Int?,
@ -265,9 +284,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState.copy(isRefreshing = true)
is Success -> oldState.copy(
isRefreshing = false,
threadList = threadList,
threadListIds = threadListIds,
goodClassifies = goodClassifies,
threadList = threadList.toImmutableList(),
threadListIds = threadListIds.toImmutableList(),
goodClassifies = goodClassifies.toImmutableList(),
goodClassifyId = goodClassifyId,
currentPage = 1,
hasMore = hasMore
@ -279,7 +298,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : Refresh()
data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>,
val threadList: List<ThreadItemData>,
val threadListIds: List<Long>,
val goodClassifies: List<ImmutableHolder<Classify>>,
val goodClassifyId: Int? = null,
@ -297,8 +316,8 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
Start -> oldState.copy(isLoadingMore = true)
is Success -> oldState.copy(
isLoadingMore = false,
threadList = oldState.threadList + threadList,
threadListIds = threadListIds,
threadList = (oldState.threadList + threadList).toImmutableList(),
threadListIds = threadListIds.toImmutableList(),
currentPage = currentPage,
hasMore = hasMore
)
@ -309,7 +328,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
object Start : LoadMore()
data class Success(
val threadList: List<ImmutableHolder<ThreadInfo>>,
val threadList: List<ThreadItemData>,
val threadListIds: List<Long>,
val currentPage: Int,
val hasMore: Boolean,
@ -321,16 +340,16 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
}
sealed class Agree private constructor() : ForumThreadListPartialChange {
private fun List<ImmutableHolder<ThreadInfo>>.updateAgreeStatus(
threadId: Long,
private fun List<ThreadItemData>.updateAgreeStatus(
id: Long,
hasAgree: Int
): List<ImmutableHolder<ThreadInfo>> {
return map { holder ->
val (threadInfo) = holder
if (threadInfo.threadId == threadId) {
threadInfo.updateAgreeStatus(hasAgree)
} else threadInfo
}.wrapImmutable()
): ImmutableList<ThreadItemData> {
return map { data ->
val (thread) = data
if (thread.get { id } == id) {
ThreadItemData(thread.getImmutable { updateAgreeStatus(hasAgree) })
} else data
}.toImmutableList()
}
override fun reduce(oldState: ForumThreadListUiState): ForumThreadListUiState =
@ -386,9 +405,9 @@ data class ForumThreadListUiState(
val isRefreshing: Boolean = false,
val isLoadingMore: Boolean = false,
val goodClassifyId: Int? = null,
val threadList: List<ImmutableHolder<ThreadInfo>> = emptyList(),
val threadListIds: List<Long> = emptyList(),
val goodClassifies: List<ImmutableHolder<Classify>> = emptyList(),
val threadList: ImmutableList<ThreadItemData> = persistentListOf(),
val threadListIds: ImmutableList<Long> = persistentListOf(),
val goodClassifies: ImmutableList<ImmutableHolder<Classify>> = persistentListOf(),
val currentPage: Int = 1,
val hasMore: Boolean = true,
) : UiState
@ -403,8 +422,11 @@ sealed interface ForumThreadListUiEvent : UiEvent {
) : ForumThreadListUiEvent
data class Refresh(
val isGood: Boolean,
val sortType: Int
) : 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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.datastore.preferences.core.booleanPreferencesKey
import com.huanchengfly.tieba.post.LocalDevicePosture
import com.huanchengfly.tieba.post.LocalNotificationCountFlow
import com.huanchengfly.tieba.post.R
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.emitGlobalEvent
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.windowsizeclass.WindowHeightSizeClass
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
@Composable
@ -100,20 +105,16 @@ fun MainPage(
navigator: DestinationsNavigator,
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(
prop1 = MainUiState::messageCount,
initial = 0
)
val eventFlows = remember {
listOf(
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
)
}
val notificationCountFlow = LocalNotificationCountFlow.current
LaunchedEffect(null) {
notificationCountFlow.collect {
@ -121,104 +122,118 @@ fun MainPage(
}
}
val hideExplore by rememberPreferenceAsState(
key = booleanPreferencesKey("hideExplore"),
defaultValue = false
)
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val themeColors = ExtendedTheme.colors
val windowSizeClass = LocalWindowSizeClass.current
val foldingDevicePosture by LocalDevicePosture.current
val navigationItems = listOfNotNull(
NavigationItem(
id = "home",
icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 },
title = { stringResource(id = R.string.title_main) },
content = {
HomePage(
eventFlow = eventFlows[0],
canOpenExplore = !LocalContext.current.appPreferences.hideExplore
) {
coroutineScope.launch {
pagerState.scrollToPage(1)
val navigationItems = remember(messageCount) {
listOfNotNull(
NavigationItem(
id = "home",
icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 },
title = { stringResource(id = R.string.title_main) },
content = {
HomePage(
canOpenExplore = !LocalContext.current.appPreferences.hideExplore
) {
coroutineScope.launch {
pagerState.scrollToPage(1)
}
}
}
}
),
if (LocalContext.current.appPreferences.hideExplore) null
else NavigationItem(
id = "explore",
icon = {
if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys) else ImageVector.vectorResource(
id = R.drawable.ic_outline_toys
)
},
title = { stringResource(id = R.string.title_explore) },
content = {
ExplorePage(eventFlows[1])
}
),
NavigationItem(
id = "notification",
icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications },
title = { stringResource(id = R.string.title_notifications) },
badge = messageCount > 0,
badgeText = "$messageCount",
onClick = {
viewModel.send(MainUiIntent.NewMessage.Clear)
},
content = {
NotificationsPage()
}
),
NavigationItem(
id = "user",
icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle },
title = { stringResource(id = R.string.title_user) },
content = {
UserPage()
}
),
).toImmutableList()
),
if (hideExplore) null
else NavigationItem(
id = "explore",
icon = {
if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys)
else ImageVector.vectorResource(id = R.drawable.ic_outline_toys)
},
title = { stringResource(id = R.string.title_explore) },
content = {
ExplorePage()
}
),
NavigationItem(
id = "notification",
icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications },
title = { stringResource(id = R.string.title_notifications) },
badge = messageCount > 0,
badgeText = "$messageCount",
onClick = {
viewModel.send(MainUiIntent.NewMessage.Clear)
},
content = {
NotificationsPage()
}
),
NavigationItem(
id = "user",
icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle },
title = { stringResource(id = R.string.title_user) },
content = {
UserPage()
}
),
).toImmutableList()
}
val navigationType = when (windowSizeClass.widthSizeClass) {
WindowWidthSizeClass.Compact -> {
MainNavigationType.BOTTOM_NAVIGATION
}
WindowWidthSizeClass.Medium -> {
MainNavigationType.NAVIGATION_RAIL
}
WindowWidthSizeClass.Expanded -> {
if (foldingDevicePosture is DevicePosture.BookPosture) {
MainNavigationType.NAVIGATION_RAIL
} else {
MainNavigationType.PERMANENT_NAVIGATION_DRAWER
val navigationType by remember {
derivedStateOf {
when (windowWidthSizeClass) {
WindowWidthSizeClass.Compact -> {
MainNavigationType.BOTTOM_NAVIGATION
}
WindowWidthSizeClass.Medium -> {
MainNavigationType.NAVIGATION_RAIL
}
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
* ergonomics and reachability depending upon the height of the device.
*/
val navigationContentPosition = when (windowSizeClass.heightSizeClass) {
WindowHeightSizeClass.Compact -> {
MainNavigationContentPosition.TOP
}
val navigationContentPosition by remember {
derivedStateOf {
when (windowHeightSizeClass) {
WindowHeightSizeClass.Compact -> {
MainNavigationContentPosition.TOP
}
WindowHeightSizeClass.Medium,
WindowHeightSizeClass.Expanded -> {
MainNavigationContentPosition.CENTER
}
WindowHeightSizeClass.Medium,
WindowHeightSizeClass.Expanded -> {
MainNavigationContentPosition.CENTER
}
else -> {
MainNavigationContentPosition.TOP
else -> {
MainNavigationContentPosition.TOP
}
}
}
}
val onReselected: (Int) -> Unit = {
coroutineScope.launch {
eventFlows[it].emit(MainUiEvent.Refresh)
}
coroutineScope.emitGlobalEvent(
GlobalEvent.Refresh(navigationItems[it].id)
)
}
NavigationWrapper(
currentPosition = pagerState.currentPage,

View File

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

View File

@ -1,8 +1,10 @@
package com.huanchengfly.tieba.post.ui.page.main.explore
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Scaffold
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.rounded.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -21,10 +24,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.R
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.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.hot.HotPage
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.accountNavIconIfCompact
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
@Immutable
data class ExplorePageItem(
val id: String,
val name: @Composable () -> Unit,
val content: @Composable () -> Unit,
)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExplorePage(
eventFlow: Flow<MainUiEvent>,
private fun ColumnScope.ExplorePageTab(
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 context = LocalContext.current
val eventFlows = remember {
listOf(
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>(),
MutableSharedFlow<MainUiEvent>()
)
val loggedIn = remember(account) { account != null }
val pages = remember {
listOfNotNull(
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 coroutineScope = rememberCoroutineScope()
eventFlow.onEvent<MainUiEvent.Refresh> {
eventFlows[pagerState.currentPage].emit(it)
onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "explore" }
) {
coroutineScope.emitGlobalEvent(GlobalEvent.Refresh(pages[pagerState.currentPage].id))
}
Scaffold(
@ -90,37 +142,7 @@ fun ExplorePage(
}
},
) {
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, 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)
}
}
},
)
}
}
ExplorePageTab(pagerState = pagerState, pages = pages)
}
},
modifier = Modifier.fillMaxSize(),
@ -129,12 +151,12 @@ fun ExplorePage(
contentPadding = paddingValues,
pageCount = pages.size,
state = pagerState,
key = { pages[it].first },
key = { pages[it].id },
modifier = Modifier.fillMaxSize(),
verticalAlignment = Alignment.Top,
userScrollEnabled = true,
) {
pages[it].second()
pages[it].content()
}
}
}

View File

@ -1,6 +1,5 @@
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.Column
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.arch.BaseComposeActivity
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.onEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.arch.wrapImmutable
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.page.LocalNavigator
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.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ConcernPage(
eventFlow: Flow<MainUiEvent>,
viewModel: ConcernViewModel = pageViewModel()
) {
LazyLoad(loaded = viewModel.initialized) {
@ -67,7 +64,9 @@ fun ConcernPage(
refreshing = isRefreshing,
onRefresh = { viewModel.send(ConcernUiIntent.Refresh) })
eventFlow.onEvent<MainUiEvent.Refresh> {
onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "concern" }
) {
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 androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.api.models.protos.ThreadInfo
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.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
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.destinations.HotTopicListPageDestination
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.LazyLoad
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.utils.StringUtil.getShortNumString
import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.flow.Flow
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class)
@Destination
@Composable
fun HotPage(
eventFlow: Flow<MainUiEvent>,
viewModel: HotViewModel = pageViewModel()
) {
LazyLoad(loaded = viewModel.initialized) {
viewModel.send(HotUiIntent.Load)
viewModel.initialized = true
}
eventFlow.onEvent<MainUiEvent.Refresh> {
onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "hot" }
) {
viewModel.send(HotUiIntent.Load)
}
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.arch.BaseComposeActivity
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
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.destinations.ForumPageDestination
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.LazyLoad
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun PersonalizedPage(
eventFlow: Flow<MainUiEvent>,
viewModel: PersonalizedViewModel = pageViewModel()
) {
LazyLoad(loaded = viewModel.initialized) {
@ -121,13 +119,11 @@ fun PersonalizedPage(
mutableStateOf(false)
}
eventFlow.onEvent<MainUiEvent.Refresh> {
onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "personalized" }
) {
viewModel.send(PersonalizedUiIntent.Refresh)
}
viewModel.onEvent<PersonalizedUiEvent.RefreshSuccess> {
refreshCount = it.count
showRefreshTip = true
}
if (showRefreshTip) {
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.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
@ -39,7 +40,6 @@ 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.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
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.activities.LoginActivity
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.onEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.arch.pageViewModel
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.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
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.compose.ActionItem
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.TiebaUtil
import com.huanchengfly.tieba.post.utils.appPreferences
import kotlinx.coroutines.flow.Flow
private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells {
return if (listSingle) {
@ -232,47 +231,22 @@ private fun ForumItemPlaceholder(
@Composable
private fun ForumItem(
viewModel: HomeViewModel,
item: HomeUiState.Forum,
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 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(
menuContent = {
if (isTopForum) {
DropdownMenuItem(
onClick = {
viewModel.send(HomeUiIntent.TopForums.Delete(item.forumId))
onDeleteTopForum(item)
menuState.expanded = false
}
) {
@ -281,7 +255,7 @@ private fun ForumItem(
} else {
DropdownMenuItem(
onClick = {
viewModel.send(HomeUiIntent.TopForums.Add(item))
onAddTopForum(item)
menuState.expanded = false
}
) {
@ -298,7 +272,7 @@ private fun ForumItem(
}
DropdownMenuItem(
onClick = {
willUnfollow = true
onUnfollow(item)
menuState.expanded = false
}
) {
@ -307,7 +281,7 @@ private fun ForumItem(
},
menuState = menuState,
onClick = {
navigator.navigate(ForumPageDestination(item.forumName))
onClick(item)
}
) {
Row(
@ -374,7 +348,6 @@ private fun ForumItem(
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun HomePage(
eventFlow: Flow<MainUiEvent>,
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
listOf(
HomeUiIntent.Refresh
@ -385,6 +358,7 @@ fun HomePage(
) {
val account = LocalAccount.current
val context = LocalContext.current
val navigator = LocalNavigator.current
val isLoading by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::isLoading,
initial = true
@ -401,14 +375,37 @@ fun HomePage(
prop1 = HomeUiState::error,
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) }
val isError by remember { derivedStateOf { error != null } }
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
eventFlow.onEvent<MainUiEvent.Refresh> {
onGlobalEvent<GlobalEvent.Refresh>(
filter = { it.key == "home" }
) {
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(
backgroundColor = Color.Transparent,
topBar = {
@ -435,7 +432,7 @@ fun HomePage(
modifier = Modifier.fillMaxSize(),
) { contentPaddings ->
StateScreen(
isEmpty = forums.isEmpty(),
isEmpty = isEmpty,
isError = isError,
isLoading = isLoading,
modifier = Modifier.padding(contentPaddings),
@ -444,7 +441,7 @@ fun HomePage(
},
emptyScreen = {
EmptyScreen(
loggedIn = account != null,
loggedIn = isLoggedIn,
canOpenExplore = canOpenExplore,
onOpenExplore = onOpenExplore
)
@ -458,7 +455,8 @@ fun HomePage(
) {
val pullRefreshState = rememberPullRefreshState(
refreshing = isLoading,
onRefresh = { viewModel.send(HomeUiIntent.Refresh) })
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }
)
Box(
modifier = Modifier
.pullRefresh(pullRefreshState)
@ -468,15 +466,14 @@ fun HomePage(
state = gridState,
columns = gridCells,
contentPadding = PaddingValues(bottom = 12.dp),
modifier = Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
) {
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
context.goToActivity<NewSearchActivity>()
}
}
if (topForums.isNotEmpty()) {
if (hasTopForum) {
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column {
Header(
@ -486,9 +483,28 @@ fun HomePage(
Spacer(modifier = Modifier.height(8.dp))
}
}
items(count = topForums.size, key = { "Top${topForums[it].forumId}" }) {
val item = topForums[it]
ForumItem(viewModel, item, listSingle, true)
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
)
}
item(
key = "Spacer",
@ -506,9 +522,27 @@ fun HomePage(
}
}
}
items(count = forums.size, key = { forums[it].forumId }) {
val item = forums[it]
ForumItem(viewModel, item, listSingle)
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))
}
)
}
}

View File

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

View File

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

View File

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