From dbd07caaec60a6ec3ee48c71e49db74073aeb39a Mon Sep 17 00:00:00 2001 From: HuanCheng65 <22636177+HuanCheng65@users.noreply.github.com> Date: Sat, 22 Jul 2023 23:17:59 +0800 Subject: [PATCH] =?UTF-8?q?pref:=20=E4=BC=98=E5=8C=96=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/huanchengfly/tieba/post/DataStore.kt | 20 ++ .../huanchengfly/tieba/post/MainActivityV2.kt | 48 ++--- .../tieba/post/arch/GlobalEvent.kt | 14 +- .../tieba/post/ui/page/forum/ForumPage.kt | 35 ++-- .../forum/threadlist/ForumThreadListPage.kt | 51 +++-- .../threadlist/ForumThreadListViewModel.kt | 78 ++++--- .../tieba/post/ui/page/main/MainPage.kt | 193 ++++++++++-------- .../post/ui/page/main/NavigationComponents.kt | 2 + .../post/ui/page/main/explore/ExplorePage.kt | 144 +++++++------ .../page/main/explore/concern/ConcernPage.kt | 13 +- .../post/ui/page/main/explore/hot/HotPage.kt | 13 +- .../explore/personalized/PersonalizedPage.kt | 14 +- .../tieba/post/ui/page/main/home/HomePage.kt | 140 ++++++++----- .../post/ui/page/main/home/HomeViewModel.kt | 5 + .../tieba/post/ui/widgets/compose/Menu.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 16 files changed, 459 insertions(+), 314 deletions(-) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/DataStore.kt b/app/src/main/java/com/huanchengfly/tieba/post/DataStore.kt index 846140cd..58edbffb 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/DataStore.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/DataStore.kt @@ -74,6 +74,26 @@ fun rememberPreferenceAsMutableState( return state } +@Composable +fun rememberPreferenceAsState( + key: Preferences.Key, + defaultValue: T +): State { + 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 DataStore.collectPreferenceAsState( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt b/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt index 627f9867..4599c740 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/MainActivityV2.kt @@ -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 { - 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) { diff --git a/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt b/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt index 9559045b..c9570a68 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/arch/GlobalEvent.kt @@ -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 by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { +private val globalEventSharedFlow: MutableSharedFlow 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 onGlobalEvent( +inline fun onGlobalEvent( coroutineScope: CoroutineScope = rememberCoroutineScope(), noinline filter: (Event) -> Boolean = { true }, noinline listener: suspend (Event) -> Unit diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt index 24853416..5b5487a0 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/ForumPage.kt @@ -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(), - MutableSharedFlow() - ) - } - 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] ) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt index b22a55c4..d08a640c 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListPage.kt @@ -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>, + items: ImmutableList, isGood: Boolean, goodClassifyId: Int?, goodClassifyHoldersProvider: () -> List>, @@ -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, isGood: Boolean = false, lazyListState: LazyListState = rememberLazyListState(), viewModel: ForumThreadListViewModel = if (isGood) pageViewModel() else pageViewModel() @@ -249,10 +270,10 @@ fun ForumThreadListPage( viewModel.send(getFirstLoadIntent(context, forumName, isGood)) viewModel.initialized = true } - eventFlow.onEvent { + onGlobalEvent { viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType)) } - eventFlow.onEvent { + onGlobalEvent { lazyListState.animateScrollToItem(0) } viewModel.onEvent { @@ -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 }, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListViewModel.kt index 16570a8e..4eebb5f3 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/forum/threadlist/ForumThreadListViewModel.kt @@ -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, + val blocked: Boolean = thread.get { shouldBlock() } +) + @Stable @HiltViewModel class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() { @@ -98,8 +109,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType ) .map { 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 { 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 { 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 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>, + val threadList: List, val threadListIds: List, val goodClassifies: List>, val goodClassifyId: Int?, @@ -265,9 +284,9 @@ sealed interface ForumThreadListPartialChange : PartialChange 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>, + val threadList: List, val threadListIds: List, val goodClassifies: List>, val goodClassifyId: Int? = null, @@ -297,8 +316,8 @@ sealed interface ForumThreadListPartialChange : PartialChange 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>, + val threadList: List, val threadListIds: List, val currentPage: Int, val hasMore: Boolean, @@ -321,16 +340,16 @@ sealed interface ForumThreadListPartialChange : PartialChange>.updateAgreeStatus( - threadId: Long, + private fun List.updateAgreeStatus( + id: Long, hasAgree: Int - ): List> { - return map { holder -> - val (threadInfo) = holder - if (threadInfo.threadId == threadId) { - threadInfo.updateAgreeStatus(hasAgree) - } else threadInfo - }.wrapImmutable() + ): ImmutableList { + 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> = emptyList(), - val threadListIds: List = emptyList(), - val goodClassifies: List> = emptyList(), + val threadList: ImmutableList = persistentListOf(), + val threadListIds: ImmutableList = persistentListOf(), + val goodClassifies: ImmutableList> = 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 } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/MainPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/MainPage.kt index bb70a78b..fc086fb8 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/MainPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/MainPage.kt @@ -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(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(), - MutableSharedFlow(), - MutableSharedFlow(), - MutableSharedFlow(), - ) - } - 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, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/NavigationComponents.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/NavigationComponents.kt index 7ed72880..4ab14a7d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/NavigationComponents.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/NavigationComponents.kt @@ -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, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/ExplorePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/ExplorePage.kt index 016614e2..e9d05b95 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/ExplorePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/ExplorePage.kt @@ -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, +private fun ColumnScope.ExplorePageTab( + pagerState: PagerState, + pages: ImmutableList ) { + 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(), - MutableSharedFlow(), - MutableSharedFlow() - ) + 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 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 { - eventFlows[pagerState.currentPage].emit(it) + onGlobalEvent( + 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() } } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt index e0b46a51..973dc31f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/concern/ConcernPage.kt @@ -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, viewModel: ConcernViewModel = pageViewModel() ) { LazyLoad(loaded = viewModel.initialized) { @@ -67,7 +64,9 @@ fun ConcernPage( refreshing = isRefreshing, onRefresh = { viewModel.send(ConcernUiIntent.Refresh) }) - eventFlow.onEvent { + onGlobalEvent( + filter = { it.key == "concern" } + ) { viewModel.send(ConcernUiIntent.Refresh) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt index 9e30190a..4bc3b3f8 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/hot/HotPage.kt @@ -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, viewModel: HotViewModel = pageViewModel() ) { LazyLoad(loaded = viewModel.initialized) { viewModel.send(HotUiIntent.Load) viewModel.initialized = true } - eventFlow.onEvent { + onGlobalEvent( + filter = { it.key == "hot" } + ) { viewModel.send(HotUiIntent.Load) } val navigator = LocalNavigator.current diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt index b803b5db..4ba803f2 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt @@ -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, viewModel: PersonalizedViewModel = pageViewModel() ) { LazyLoad(loaded = viewModel.initialized) { @@ -121,13 +119,11 @@ fun PersonalizedPage( mutableStateOf(false) } - eventFlow.onEvent { + onGlobalEvent( + filter = { it.key == "personalized" } + ) { viewModel.send(PersonalizedUiIntent.Refresh) } - viewModel.onEvent { - refreshCount = it.count - showRefreshTip = true - } if (showRefreshTip) { LaunchedEffect(Unit) { diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt index 8b1e2cf3..9cf78fcc 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt @@ -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, viewModel: HomeViewModel = pageViewModel( 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 { + onGlobalEvent( + filter = { it.key == "home" } + ) { viewModel.send(HomeUiIntent.Refresh) } + var unfollowForum by remember { mutableStateOf(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() } } - 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)) + } + ) } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt index 5a183cf6..947aa2a4 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt @@ -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() { override fun createInitialState(): HomeUiState = HomeUiState() @@ -184,12 +187,14 @@ sealed interface HomePartialChange : PartialChange { } } +@Immutable data class HomeUiState( val isLoading: Boolean = true, val forums: List = emptyList(), val topForums: List = emptyList(), val error: Throwable? = null, ) : UiState { + @Immutable data class Forum( val avatar: String, val forumId: String, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt index 20e223d7..aed0ab02 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt @@ -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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f159f831..824607d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -706,4 +706,5 @@ 以上为最新回复 以下为最新回复 查看原贴 + 由于你的屏蔽设置,该贴已被屏蔽