pref: 优化首页性能
This commit is contained in:
parent
3fe7aee173
commit
dbd07caaec
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue