pref: 优化首页性能
This commit is contained in:
parent
3fe7aee173
commit
dbd07caaec
|
|
@ -74,6 +74,26 @@ fun <T> rememberPreferenceAsMutableState(
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> rememberPreferenceAsState(
|
||||||
|
key: Preferences.Key<T>,
|
||||||
|
defaultValue: T
|
||||||
|
): State<T> {
|
||||||
|
val dataStore = LocalContext.current.dataStore
|
||||||
|
val state = remember { mutableStateOf(defaultValue) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
dataStore.data.map { it[key] ?: defaultValue }.distinctUntilChanged()
|
||||||
|
.collect { state.value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(state.value) {
|
||||||
|
dataStore.edit { it[key] = state.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("FlowOperatorInvokedInComposition")
|
@SuppressLint("FlowOperatorInvokedInComposition")
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> DataStore<Preferences>.collectPreferenceAsState(
|
fun <T> DataStore<Preferences>.collectPreferenceAsState(
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
|
@ -279,30 +280,17 @@ class MainActivityV2 : BaseComposeActivity() {
|
||||||
LocalDevicePosture provides devicePostureFlow.collectAsState(),
|
LocalDevicePosture provides devicePostureFlow.collectAsState(),
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
if (ThemeUtil.isTranslucentTheme(ExtendedTheme.colors.theme)) {
|
TranslucentThemeBackground()
|
||||||
val backgroundPath by rememberPreferenceAsMutableState(
|
|
||||||
key = stringPreferencesKey(
|
|
||||||
"translucent_theme_background_path"
|
|
||||||
),
|
|
||||||
defaultValue = ""
|
|
||||||
)
|
|
||||||
if (backgroundPath.isNotEmpty()) {
|
|
||||||
AsyncImage(
|
|
||||||
imageUri = newFileUri(backgroundPath),
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentScale = ContentScale.Crop
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
color = ExtendedTheme.colors.background
|
color = ExtendedTheme.colors.background
|
||||||
) {
|
) {
|
||||||
val animationSpec = spring(
|
val animationSpec = remember {
|
||||||
stiffness = Spring.StiffnessMediumLow,
|
spring(
|
||||||
visibilityThreshold = IntOffset.VisibilityThreshold
|
stiffness = Spring.StiffnessMediumLow,
|
||||||
)
|
visibilityThreshold = IntOffset.VisibilityThreshold
|
||||||
|
)
|
||||||
|
}
|
||||||
val engine = rememberAnimatedNavHostEngine(
|
val engine = rememberAnimatedNavHostEngine(
|
||||||
navHostContentAlignment = Alignment.TopStart,
|
navHostContentAlignment = Alignment.TopStart,
|
||||||
rootDefaultAnimations = RootNavGraphDefaultAnimations(
|
rootDefaultAnimations = RootNavGraphDefaultAnimations(
|
||||||
|
|
@ -337,9 +325,6 @@ class MainActivityV2 : BaseComposeActivity() {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val navController = rememberAnimatedNavController()
|
val navController = rememberAnimatedNavController()
|
||||||
onGlobalEvent<GlobalEvent.NavigateUp> {
|
|
||||||
navController.navigateUp()
|
|
||||||
}
|
|
||||||
val bottomSheetNavigator =
|
val bottomSheetNavigator =
|
||||||
rememberBottomSheetNavigator(
|
rememberBottomSheetNavigator(
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
|
|
@ -373,6 +358,23 @@ class MainActivityV2 : BaseComposeActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TranslucentThemeBackground() {
|
||||||
|
if (ThemeUtil.isTranslucentTheme(ExtendedTheme.colors.theme)) {
|
||||||
|
val backgroundPath by rememberPreferenceAsMutableState(
|
||||||
|
key = stringPreferencesKey("translucent_theme_background_path"),
|
||||||
|
defaultValue = ""
|
||||||
|
)
|
||||||
|
val backgroundUri by remember { derivedStateOf { newFileUri(backgroundPath) } }
|
||||||
|
AsyncImage(
|
||||||
|
imageUri = backgroundUri,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class NewMessageReceiver : BroadcastReceiver() {
|
private inner class NewMessageReceiver : BroadcastReceiver() {
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,13 @@ import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed interface GlobalEvent {
|
sealed interface GlobalEvent : UiEvent {
|
||||||
object AccountSwitched : GlobalEvent
|
object AccountSwitched : GlobalEvent
|
||||||
|
|
||||||
object NavigateUp : GlobalEvent
|
object NavigateUp : GlobalEvent
|
||||||
|
|
||||||
|
data class Refresh(val key: String) : GlobalEvent
|
||||||
|
|
||||||
data class StartSelectImages(
|
data class StartSelectImages(
|
||||||
val id: String,
|
val id: String,
|
||||||
val maxCount: Int,
|
val maxCount: Int,
|
||||||
|
|
@ -40,20 +42,24 @@ sealed interface GlobalEvent {
|
||||||
) : GlobalEvent
|
) : GlobalEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
private val globalEventSharedFlow: MutableSharedFlow<GlobalEvent> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
|
private val globalEventSharedFlow: MutableSharedFlow<UiEvent> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||||
MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
|
MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
val GlobalEventFlow = globalEventSharedFlow.asSharedFlow()
|
val GlobalEventFlow = globalEventSharedFlow.asSharedFlow()
|
||||||
|
|
||||||
fun CoroutineScope.emitGlobalEvent(event: GlobalEvent) {
|
fun CoroutineScope.emitGlobalEvent(event: UiEvent) {
|
||||||
launch {
|
launch {
|
||||||
globalEventSharedFlow.emit(event)
|
globalEventSharedFlow.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun emitGlobalEvent(event: UiEvent) {
|
||||||
|
globalEventSharedFlow.emit(event)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <reified Event : GlobalEvent> onGlobalEvent(
|
inline fun <reified Event : UiEvent> onGlobalEvent(
|
||||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||||
noinline filter: (Event) -> Boolean = { true },
|
noinline filter: (Event) -> Boolean = { true },
|
||||||
noinline listener: suspend (Event) -> Unit
|
noinline listener: suspend (Event) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ import com.huanchengfly.tieba.post.activities.SearchPostActivity
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.frsPage.ForumInfo
|
import com.huanchengfly.tieba.post.api.models.protos.frsPage.ForumInfo
|
||||||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
|
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.dataStore
|
import com.huanchengfly.tieba.post.dataStore
|
||||||
|
|
@ -99,6 +100,7 @@ import com.huanchengfly.tieba.post.getInt
|
||||||
import com.huanchengfly.tieba.post.goToActivity
|
import com.huanchengfly.tieba.post.goToActivity
|
||||||
import com.huanchengfly.tieba.post.models.database.History
|
import com.huanchengfly.tieba.post.models.database.History
|
||||||
import com.huanchengfly.tieba.post.pxToDp
|
import com.huanchengfly.tieba.post.pxToDp
|
||||||
|
import com.huanchengfly.tieba.post.toastShort
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
||||||
|
|
@ -132,7 +134,6 @@ import com.ramcosta.composedestinations.annotation.DeepLink
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
@ -468,13 +469,6 @@ fun ForumPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val eventFlows = remember {
|
|
||||||
listOf(
|
|
||||||
MutableSharedFlow<ForumThreadListUiEvent>(),
|
|
||||||
MutableSharedFlow<ForumThreadListUiEvent>()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val unlikeDialogState = rememberDialogState()
|
val unlikeDialogState = rememberDialogState()
|
||||||
|
|
||||||
LaunchedEffect(forumInfo) {
|
LaunchedEffect(forumInfo) {
|
||||||
|
|
@ -600,11 +594,14 @@ fun ForumPage(
|
||||||
when (context.appPreferences.forumFabFunction) {
|
when (context.appPreferences.forumFabFunction) {
|
||||||
"refresh" -> {
|
"refresh" -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
eventFlows[pagerState.currentPage].emit(
|
emitGlobalEvent(
|
||||||
ForumThreadListUiEvent.BackToTop
|
ForumThreadListUiEvent.BackToTop(
|
||||||
|
pagerState.currentPage == 1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
eventFlows[pagerState.currentPage].emit(
|
emitGlobalEvent(
|
||||||
ForumThreadListUiEvent.Refresh(
|
ForumThreadListUiEvent.Refresh(
|
||||||
|
pagerState.currentPage == 1,
|
||||||
getSortType(
|
getSortType(
|
||||||
context,
|
context,
|
||||||
forumName
|
forumName
|
||||||
|
|
@ -616,14 +613,16 @@ fun ForumPage(
|
||||||
|
|
||||||
"back_to_top" -> {
|
"back_to_top" -> {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
eventFlows[pagerState.currentPage].emit(
|
emitGlobalEvent(
|
||||||
ForumThreadListUiEvent.BackToTop
|
ForumThreadListUiEvent.BackToTop(
|
||||||
|
pagerState.currentPage == 1
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
context.toastShort(R.string.toast_feature_unavailable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -752,8 +751,11 @@ fun ForumPage(
|
||||||
setSortType(context, forumName, value)
|
setSortType(context, forumName, value)
|
||||||
}
|
}
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
eventFlows[pagerState.currentPage].emit(
|
emitGlobalEvent(
|
||||||
ForumThreadListUiEvent.Refresh(value)
|
ForumThreadListUiEvent.Refresh(
|
||||||
|
pagerState.currentPage == 1,
|
||||||
|
value
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
currentSortType = value
|
currentSortType = value
|
||||||
|
|
@ -837,7 +839,6 @@ fun ForumPage(
|
||||||
ForumThreadListPage(
|
ForumThreadListPage(
|
||||||
forumId = forumInfo!!.get { id },
|
forumId = forumInfo!!.get { id },
|
||||||
forumName = forumInfo!!.get { name },
|
forumName = forumInfo!!.get { name },
|
||||||
eventFlow = eventFlows[it],
|
|
||||||
isGood = it == 1,
|
isGood = it == 1,
|
||||||
lazyListState = lazyListStates[it]
|
lazyListState = lazyListStates[it]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
|
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -45,6 +46,7 @@ import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindo
|
||||||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
|
|
@ -58,7 +60,9 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LocalSnackbarHostState
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
||||||
import kotlinx.coroutines.flow.Flow
|
import com.huanchengfly.tieba.post.utils.appPreferences
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
private fun getFirstLoadIntent(
|
private fun getFirstLoadIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -133,7 +137,7 @@ private fun GoodClassifyTabs(
|
||||||
@Composable
|
@Composable
|
||||||
private fun ThreadList(
|
private fun ThreadList(
|
||||||
state: LazyListState,
|
state: LazyListState,
|
||||||
itemHoldersProvider: () -> List<ImmutableHolder<ThreadInfo>>,
|
items: ImmutableList<ThreadItemData>,
|
||||||
isGood: Boolean,
|
isGood: Boolean,
|
||||||
goodClassifyId: Int?,
|
goodClassifyId: Int?,
|
||||||
goodClassifyHoldersProvider: () -> List<ImmutableHolder<Classify>>,
|
goodClassifyHoldersProvider: () -> List<ImmutableHolder<Classify>>,
|
||||||
|
|
@ -142,7 +146,6 @@ private fun ThreadList(
|
||||||
onAgree: (ThreadInfo) -> Unit,
|
onAgree: (ThreadInfo) -> Unit,
|
||||||
onClassifySelected: (Int) -> Unit
|
onClassifySelected: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val itemHolders = itemHoldersProvider()
|
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
val itemFraction = when (windowSizeClass.widthSizeClass) {
|
val itemFraction = when (windowSizeClass.widthSizeClass) {
|
||||||
WindowWidthSizeClass.Expanded -> 0.5f
|
WindowWidthSizeClass.Expanded -> 0.5f
|
||||||
|
|
@ -164,12 +167,12 @@ private fun ThreadList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = itemHolders,
|
items = items,
|
||||||
key = { index, holder ->
|
key = { index, (holder) ->
|
||||||
val (item) = holder
|
val (item) = holder
|
||||||
"${index}_${item.id}"
|
"${index}_${item.id}"
|
||||||
},
|
},
|
||||||
contentType = { _, holder ->
|
contentType = { _, (holder) ->
|
||||||
val (item) = holder
|
val (item) = holder
|
||||||
if (item.isTop == 1) ItemType.Top
|
if (item.isTop == 1) ItemType.Top
|
||||||
else {
|
else {
|
||||||
|
|
@ -180,7 +183,26 @@ private fun ThreadList(
|
||||||
else ItemType.PlainText
|
else ItemType.PlainText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { index, holder ->
|
) { index, (holder, blocked) ->
|
||||||
|
if (blocked) {
|
||||||
|
if (!LocalContext.current.appPreferences.hideBlockedContent) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(6.dp))
|
||||||
|
.background(ExtendedTheme.colors.floorCard)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.tip_blocked_thread),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = ExtendedTheme.colors.textSecondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return@itemsIndexed
|
||||||
|
}
|
||||||
val (item) = holder
|
val (item) = holder
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(itemFraction)
|
modifier = Modifier.fillMaxWidth(itemFraction)
|
||||||
|
|
@ -215,7 +237,7 @@ private fun ThreadList(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
if (itemHolders[index - 1].item.isTop == 1) {
|
if (items[index - 1].thread.get { isTop } == 1) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
VerticalDivider(modifier = Modifier.padding(horizontal = 16.dp))
|
||||||
|
|
@ -237,7 +259,6 @@ private fun ThreadList(
|
||||||
fun ForumThreadListPage(
|
fun ForumThreadListPage(
|
||||||
forumId: Long,
|
forumId: Long,
|
||||||
forumName: String,
|
forumName: String,
|
||||||
eventFlow: Flow<ForumThreadListUiEvent>,
|
|
||||||
isGood: Boolean = false,
|
isGood: Boolean = false,
|
||||||
lazyListState: LazyListState = rememberLazyListState(),
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>()
|
viewModel: ForumThreadListViewModel = if (isGood) pageViewModel<GoodThreadListViewModel>() else pageViewModel<LatestThreadListViewModel>()
|
||||||
|
|
@ -249,10 +270,10 @@ fun ForumThreadListPage(
|
||||||
viewModel.send(getFirstLoadIntent(context, forumName, isGood))
|
viewModel.send(getFirstLoadIntent(context, forumName, isGood))
|
||||||
viewModel.initialized = true
|
viewModel.initialized = true
|
||||||
}
|
}
|
||||||
eventFlow.onEvent<ForumThreadListUiEvent.Refresh> {
|
onGlobalEvent<ForumThreadListUiEvent.Refresh> {
|
||||||
viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType))
|
viewModel.send(getRefreshIntent(context, forumName, isGood, it.sortType))
|
||||||
}
|
}
|
||||||
eventFlow.onEvent<ForumThreadListUiEvent.BackToTop> {
|
onGlobalEvent<ForumThreadListUiEvent.BackToTop> {
|
||||||
lazyListState.animateScrollToItem(0)
|
lazyListState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
viewModel.onEvent<ForumThreadListUiEvent.AgreeFail> {
|
viewModel.onEvent<ForumThreadListUiEvent.AgreeFail> {
|
||||||
|
|
@ -293,11 +314,11 @@ fun ForumThreadListPage(
|
||||||
)
|
)
|
||||||
val threadList by viewModel.uiState.collectPartialAsState(
|
val threadList by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = ForumThreadListUiState::threadList,
|
prop1 = ForumThreadListUiState::threadList,
|
||||||
initial = emptyList()
|
initial = persistentListOf()
|
||||||
)
|
)
|
||||||
val threadListIds by viewModel.uiState.collectPartialAsState(
|
val threadListIds by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = ForumThreadListUiState::threadListIds,
|
prop1 = ForumThreadListUiState::threadListIds,
|
||||||
initial = emptyList()
|
initial = persistentListOf()
|
||||||
)
|
)
|
||||||
val goodClassifyId by viewModel.uiState.collectPartialAsState(
|
val goodClassifyId by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = ForumThreadListUiState::goodClassifyId,
|
prop1 = ForumThreadListUiState::goodClassifyId,
|
||||||
|
|
@ -305,7 +326,7 @@ fun ForumThreadListPage(
|
||||||
)
|
)
|
||||||
val goodClassifies by viewModel.uiState.collectPartialAsState(
|
val goodClassifies by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = ForumThreadListUiState::goodClassifies,
|
prop1 = ForumThreadListUiState::goodClassifies,
|
||||||
initial = emptyList()
|
initial = persistentListOf()
|
||||||
)
|
)
|
||||||
val pullRefreshState = rememberPullRefreshState(
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
refreshing = isRefreshing,
|
refreshing = isRefreshing,
|
||||||
|
|
@ -330,7 +351,7 @@ fun ForumThreadListPage(
|
||||||
) {
|
) {
|
||||||
ThreadList(
|
ThreadList(
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
itemHoldersProvider = { threadList },
|
items = threadList,
|
||||||
isGood = isGood,
|
isGood = isGood,
|
||||||
goodClassifyId = goodClassifyId,
|
goodClassifyId = goodClassifyId,
|
||||||
goodClassifyHoldersProvider = { goodClassifies },
|
goodClassifyHoldersProvider = { goodClassifies },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
|
package com.huanchengfly.tieba.post.ui.page.forum.threadlist
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.huanchengfly.tieba.post.api.TiebaApi
|
import com.huanchengfly.tieba.post.api.TiebaApi
|
||||||
import com.huanchengfly.tieba.post.api.models.AgreeBean
|
import com.huanchengfly.tieba.post.api.models.AgreeBean
|
||||||
|
|
@ -20,7 +21,11 @@ import com.huanchengfly.tieba.post.arch.UiIntent
|
||||||
import com.huanchengfly.tieba.post.arch.UiState
|
import com.huanchengfly.tieba.post.arch.UiState
|
||||||
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
||||||
import com.huanchengfly.tieba.post.repository.FrsPageRepository
|
import com.huanchengfly.tieba.post.repository.FrsPageRepository
|
||||||
|
import com.huanchengfly.tieba.post.utils.BlockManager.shouldBlock
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
|
|
@ -59,6 +64,12 @@ enum class ForumThreadListType {
|
||||||
Latest, Good
|
Latest, Good
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ThreadItemData(
|
||||||
|
val thread: ImmutableHolder<ThreadInfo>,
|
||||||
|
val blocked: Boolean = thread.get { shouldBlock() }
|
||||||
|
)
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() {
|
class LatestThreadListViewModel @Inject constructor() : ForumThreadListViewModel() {
|
||||||
|
|
@ -98,8 +109,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
|
||||||
)
|
)
|
||||||
.map<FrsPageResponse, ForumThreadListPartialChange.FirstLoad> { response ->
|
.map<FrsPageResponse, ForumThreadListPartialChange.FirstLoad> { response ->
|
||||||
if (response.data_?.page == null) throw TiebaUnknownException
|
if (response.data_?.page == null) throw TiebaUnknownException
|
||||||
|
val threadList =
|
||||||
|
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
|
||||||
ForumThreadListPartialChange.FirstLoad.Success(
|
ForumThreadListPartialChange.FirstLoad.Success(
|
||||||
response.data_.thread_list.wrapImmutable(),
|
threadList,
|
||||||
response.data_.thread_id_list,
|
response.data_.thread_id_list,
|
||||||
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
|
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
|
||||||
goodClassifyId.takeIf { type == ForumThreadListType.Good },
|
goodClassifyId.takeIf { type == ForumThreadListType.Good },
|
||||||
|
|
@ -120,8 +133,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
|
||||||
)
|
)
|
||||||
.map<FrsPageResponse, ForumThreadListPartialChange.Refresh> { response ->
|
.map<FrsPageResponse, ForumThreadListPartialChange.Refresh> { response ->
|
||||||
if (response.data_?.page == null) throw TiebaUnknownException
|
if (response.data_?.page == null) throw TiebaUnknownException
|
||||||
|
val threadList =
|
||||||
|
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
|
||||||
ForumThreadListPartialChange.Refresh.Success(
|
ForumThreadListPartialChange.Refresh.Success(
|
||||||
response.data_.thread_list.wrapImmutable(),
|
threadList,
|
||||||
response.data_.thread_id_list,
|
response.data_.thread_id_list,
|
||||||
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
|
(response.data_.forum?.good_classify ?: emptyList()).wrapImmutable(),
|
||||||
goodClassifyId.takeIf { type == ForumThreadListType.Good },
|
goodClassifyId.takeIf { type == ForumThreadListType.Good },
|
||||||
|
|
@ -142,8 +157,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
|
||||||
threadListIds.subList(0, size).joinToString(separator = ",") { "$it" }
|
threadListIds.subList(0, size).joinToString(separator = ",") { "$it" }
|
||||||
).map { response ->
|
).map { response ->
|
||||||
if (response.data_ == null) throw TiebaUnknownException
|
if (response.data_ == null) throw TiebaUnknownException
|
||||||
|
val threadList =
|
||||||
|
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
|
||||||
ForumThreadListPartialChange.LoadMore.Success(
|
ForumThreadListPartialChange.LoadMore.Success(
|
||||||
threadList = response.data_.thread_list.wrapImmutable(),
|
threadList = threadList,
|
||||||
threadListIds = threadListIds.drop(size),
|
threadListIds = threadListIds.drop(size),
|
||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
hasMore = response.data_.thread_list.isNotEmpty()
|
hasMore = response.data_.thread_list.isNotEmpty()
|
||||||
|
|
@ -159,8 +176,10 @@ private class ForumThreadListPartialChangeProducer(val type: ForumThreadListType
|
||||||
)
|
)
|
||||||
.map<FrsPageResponse, ForumThreadListPartialChange.LoadMore> { response ->
|
.map<FrsPageResponse, ForumThreadListPartialChange.LoadMore> { response ->
|
||||||
if (response.data_?.page == null) throw TiebaUnknownException
|
if (response.data_?.page == null) throw TiebaUnknownException
|
||||||
|
val threadList =
|
||||||
|
response.data_.thread_list.map { ThreadItemData(it.wrapImmutable()) }
|
||||||
ForumThreadListPartialChange.LoadMore.Success(
|
ForumThreadListPartialChange.LoadMore.Success(
|
||||||
threadList = response.data_.thread_list.wrapImmutable(),
|
threadList = threadList,
|
||||||
threadListIds = response.data_.thread_id_list,
|
threadListIds = response.data_.thread_id_list,
|
||||||
currentPage = currentPage + 1,
|
currentPage = currentPage + 1,
|
||||||
response.data_.page.has_more == 1
|
response.data_.page.has_more == 1
|
||||||
|
|
@ -233,9 +252,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
Start -> oldState
|
Start -> oldState
|
||||||
is Success -> oldState.copy(
|
is Success -> oldState.copy(
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
threadList = threadList,
|
threadList = threadList.toImmutableList(),
|
||||||
threadListIds = threadListIds,
|
threadListIds = threadListIds.toImmutableList(),
|
||||||
goodClassifies = goodClassifies,
|
goodClassifies = goodClassifies.toImmutableList(),
|
||||||
goodClassifyId = goodClassifyId,
|
goodClassifyId = goodClassifyId,
|
||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
hasMore = hasMore
|
hasMore = hasMore
|
||||||
|
|
@ -247,7 +266,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
object Start : FirstLoad()
|
object Start : FirstLoad()
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val threadList: List<ImmutableHolder<ThreadInfo>>,
|
val threadList: List<ThreadItemData>,
|
||||||
val threadListIds: List<Long>,
|
val threadListIds: List<Long>,
|
||||||
val goodClassifies: List<ImmutableHolder<Classify>>,
|
val goodClassifies: List<ImmutableHolder<Classify>>,
|
||||||
val goodClassifyId: Int?,
|
val goodClassifyId: Int?,
|
||||||
|
|
@ -265,9 +284,9 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
Start -> oldState.copy(isRefreshing = true)
|
Start -> oldState.copy(isRefreshing = true)
|
||||||
is Success -> oldState.copy(
|
is Success -> oldState.copy(
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
threadList = threadList,
|
threadList = threadList.toImmutableList(),
|
||||||
threadListIds = threadListIds,
|
threadListIds = threadListIds.toImmutableList(),
|
||||||
goodClassifies = goodClassifies,
|
goodClassifies = goodClassifies.toImmutableList(),
|
||||||
goodClassifyId = goodClassifyId,
|
goodClassifyId = goodClassifyId,
|
||||||
currentPage = 1,
|
currentPage = 1,
|
||||||
hasMore = hasMore
|
hasMore = hasMore
|
||||||
|
|
@ -279,7 +298,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
object Start : Refresh()
|
object Start : Refresh()
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val threadList: List<ImmutableHolder<ThreadInfo>>,
|
val threadList: List<ThreadItemData>,
|
||||||
val threadListIds: List<Long>,
|
val threadListIds: List<Long>,
|
||||||
val goodClassifies: List<ImmutableHolder<Classify>>,
|
val goodClassifies: List<ImmutableHolder<Classify>>,
|
||||||
val goodClassifyId: Int? = null,
|
val goodClassifyId: Int? = null,
|
||||||
|
|
@ -297,8 +316,8 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
Start -> oldState.copy(isLoadingMore = true)
|
Start -> oldState.copy(isLoadingMore = true)
|
||||||
is Success -> oldState.copy(
|
is Success -> oldState.copy(
|
||||||
isLoadingMore = false,
|
isLoadingMore = false,
|
||||||
threadList = oldState.threadList + threadList,
|
threadList = (oldState.threadList + threadList).toImmutableList(),
|
||||||
threadListIds = threadListIds,
|
threadListIds = threadListIds.toImmutableList(),
|
||||||
currentPage = currentPage,
|
currentPage = currentPage,
|
||||||
hasMore = hasMore
|
hasMore = hasMore
|
||||||
)
|
)
|
||||||
|
|
@ -309,7 +328,7 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
object Start : LoadMore()
|
object Start : LoadMore()
|
||||||
|
|
||||||
data class Success(
|
data class Success(
|
||||||
val threadList: List<ImmutableHolder<ThreadInfo>>,
|
val threadList: List<ThreadItemData>,
|
||||||
val threadListIds: List<Long>,
|
val threadListIds: List<Long>,
|
||||||
val currentPage: Int,
|
val currentPage: Int,
|
||||||
val hasMore: Boolean,
|
val hasMore: Boolean,
|
||||||
|
|
@ -321,16 +340,16 @@ sealed interface ForumThreadListPartialChange : PartialChange<ForumThreadListUiS
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Agree private constructor() : ForumThreadListPartialChange {
|
sealed class Agree private constructor() : ForumThreadListPartialChange {
|
||||||
private fun List<ImmutableHolder<ThreadInfo>>.updateAgreeStatus(
|
private fun List<ThreadItemData>.updateAgreeStatus(
|
||||||
threadId: Long,
|
id: Long,
|
||||||
hasAgree: Int
|
hasAgree: Int
|
||||||
): List<ImmutableHolder<ThreadInfo>> {
|
): ImmutableList<ThreadItemData> {
|
||||||
return map { holder ->
|
return map { data ->
|
||||||
val (threadInfo) = holder
|
val (thread) = data
|
||||||
if (threadInfo.threadId == threadId) {
|
if (thread.get { id } == id) {
|
||||||
threadInfo.updateAgreeStatus(hasAgree)
|
ThreadItemData(thread.getImmutable { updateAgreeStatus(hasAgree) })
|
||||||
} else threadInfo
|
} else data
|
||||||
}.wrapImmutable()
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun reduce(oldState: ForumThreadListUiState): ForumThreadListUiState =
|
override fun reduce(oldState: ForumThreadListUiState): ForumThreadListUiState =
|
||||||
|
|
@ -386,9 +405,9 @@ data class ForumThreadListUiState(
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val isLoadingMore: Boolean = false,
|
val isLoadingMore: Boolean = false,
|
||||||
val goodClassifyId: Int? = null,
|
val goodClassifyId: Int? = null,
|
||||||
val threadList: List<ImmutableHolder<ThreadInfo>> = emptyList(),
|
val threadList: ImmutableList<ThreadItemData> = persistentListOf(),
|
||||||
val threadListIds: List<Long> = emptyList(),
|
val threadListIds: ImmutableList<Long> = persistentListOf(),
|
||||||
val goodClassifies: List<ImmutableHolder<Classify>> = emptyList(),
|
val goodClassifies: ImmutableList<ImmutableHolder<Classify>> = persistentListOf(),
|
||||||
val currentPage: Int = 1,
|
val currentPage: Int = 1,
|
||||||
val hasMore: Boolean = true,
|
val hasMore: Boolean = true,
|
||||||
) : UiState
|
) : UiState
|
||||||
|
|
@ -403,8 +422,11 @@ sealed interface ForumThreadListUiEvent : UiEvent {
|
||||||
) : ForumThreadListUiEvent
|
) : ForumThreadListUiEvent
|
||||||
|
|
||||||
data class Refresh(
|
data class Refresh(
|
||||||
|
val isGood: Boolean,
|
||||||
val sortType: Int
|
val sortType: Int
|
||||||
) : ForumThreadListUiEvent
|
) : ForumThreadListUiEvent
|
||||||
|
|
||||||
object BackToTop : ForumThreadListUiEvent
|
data class BackToTop(
|
||||||
|
val isGood: Boolean
|
||||||
|
) : ForumThreadListUiEvent
|
||||||
}
|
}
|
||||||
|
|
@ -18,9 +18,11 @@ import androidx.compose.material.icons.rounded.Inventory2
|
||||||
import androidx.compose.material.icons.rounded.Notifications
|
import androidx.compose.material.icons.rounded.Notifications
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -28,12 +30,16 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import com.huanchengfly.tieba.post.LocalDevicePosture
|
import com.huanchengfly.tieba.post.LocalDevicePosture
|
||||||
import com.huanchengfly.tieba.post.LocalNotificationCountFlow
|
import com.huanchengfly.tieba.post.LocalNotificationCountFlow
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
|
import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass
|
||||||
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
|
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
|
import com.huanchengfly.tieba.post.rememberPreferenceAsState
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowHeightSizeClass
|
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowHeightSizeClass
|
||||||
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
|
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
|
||||||
|
|
@ -52,7 +58,6 @@ import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -100,20 +105,16 @@ fun MainPage(
|
||||||
navigator: DestinationsNavigator,
|
navigator: DestinationsNavigator,
|
||||||
viewModel: MainViewModel = pageViewModel<MainUiIntent, MainViewModel>(emptyList()),
|
viewModel: MainViewModel = pageViewModel<MainUiIntent, MainViewModel>(emptyList()),
|
||||||
) {
|
) {
|
||||||
|
val windowSizeClass = LocalWindowSizeClass.current
|
||||||
|
val windowHeightSizeClass by rememberUpdatedState(newValue = windowSizeClass.heightSizeClass)
|
||||||
|
val windowWidthSizeClass by rememberUpdatedState(newValue = windowSizeClass.widthSizeClass)
|
||||||
|
val foldingDevicePosture by LocalDevicePosture.current
|
||||||
|
|
||||||
val messageCount by viewModel.uiState.collectPartialAsState(
|
val messageCount by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = MainUiState::messageCount,
|
prop1 = MainUiState::messageCount,
|
||||||
initial = 0
|
initial = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
val eventFlows = remember {
|
|
||||||
listOf(
|
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val notificationCountFlow = LocalNotificationCountFlow.current
|
val notificationCountFlow = LocalNotificationCountFlow.current
|
||||||
LaunchedEffect(null) {
|
LaunchedEffect(null) {
|
||||||
notificationCountFlow.collect {
|
notificationCountFlow.collect {
|
||||||
|
|
@ -121,104 +122,118 @@ fun MainPage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hideExplore by rememberPreferenceAsState(
|
||||||
|
key = booleanPreferencesKey("hideExplore"),
|
||||||
|
defaultValue = false
|
||||||
|
)
|
||||||
|
|
||||||
val pagerState = rememberPagerState()
|
val pagerState = rememberPagerState()
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val themeColors = ExtendedTheme.colors
|
val themeColors = ExtendedTheme.colors
|
||||||
val windowSizeClass = LocalWindowSizeClass.current
|
val navigationItems = remember(messageCount) {
|
||||||
val foldingDevicePosture by LocalDevicePosture.current
|
listOfNotNull(
|
||||||
val navigationItems = listOfNotNull(
|
NavigationItem(
|
||||||
NavigationItem(
|
id = "home",
|
||||||
id = "home",
|
icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 },
|
||||||
icon = { if (it) Icons.Rounded.Inventory2 else Icons.Outlined.Inventory2 },
|
title = { stringResource(id = R.string.title_main) },
|
||||||
title = { stringResource(id = R.string.title_main) },
|
content = {
|
||||||
content = {
|
HomePage(
|
||||||
HomePage(
|
canOpenExplore = !LocalContext.current.appPreferences.hideExplore
|
||||||
eventFlow = eventFlows[0],
|
) {
|
||||||
canOpenExplore = !LocalContext.current.appPreferences.hideExplore
|
coroutineScope.launch {
|
||||||
) {
|
pagerState.scrollToPage(1)
|
||||||
coroutineScope.launch {
|
}
|
||||||
pagerState.scrollToPage(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
),
|
||||||
),
|
if (hideExplore) null
|
||||||
if (LocalContext.current.appPreferences.hideExplore) null
|
else NavigationItem(
|
||||||
else NavigationItem(
|
id = "explore",
|
||||||
id = "explore",
|
icon = {
|
||||||
icon = {
|
if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys)
|
||||||
if (it) ImageVector.vectorResource(id = R.drawable.ic_round_toys) else ImageVector.vectorResource(
|
else ImageVector.vectorResource(id = R.drawable.ic_outline_toys)
|
||||||
id = R.drawable.ic_outline_toys
|
},
|
||||||
)
|
title = { stringResource(id = R.string.title_explore) },
|
||||||
},
|
content = {
|
||||||
title = { stringResource(id = R.string.title_explore) },
|
ExplorePage()
|
||||||
content = {
|
}
|
||||||
ExplorePage(eventFlows[1])
|
),
|
||||||
}
|
NavigationItem(
|
||||||
),
|
id = "notification",
|
||||||
NavigationItem(
|
icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications },
|
||||||
id = "notification",
|
title = { stringResource(id = R.string.title_notifications) },
|
||||||
icon = { if (it) Icons.Rounded.Notifications else Icons.Outlined.Notifications },
|
badge = messageCount > 0,
|
||||||
title = { stringResource(id = R.string.title_notifications) },
|
badgeText = "$messageCount",
|
||||||
badge = messageCount > 0,
|
onClick = {
|
||||||
badgeText = "$messageCount",
|
viewModel.send(MainUiIntent.NewMessage.Clear)
|
||||||
onClick = {
|
},
|
||||||
viewModel.send(MainUiIntent.NewMessage.Clear)
|
content = {
|
||||||
},
|
NotificationsPage()
|
||||||
content = {
|
}
|
||||||
NotificationsPage()
|
),
|
||||||
}
|
NavigationItem(
|
||||||
),
|
id = "user",
|
||||||
NavigationItem(
|
icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle },
|
||||||
id = "user",
|
title = { stringResource(id = R.string.title_user) },
|
||||||
icon = { if (it) Icons.Rounded.AccountCircle else Icons.Outlined.AccountCircle },
|
content = {
|
||||||
title = { stringResource(id = R.string.title_user) },
|
UserPage()
|
||||||
content = {
|
}
|
||||||
UserPage()
|
),
|
||||||
}
|
).toImmutableList()
|
||||||
),
|
}
|
||||||
).toImmutableList()
|
|
||||||
|
|
||||||
val navigationType = when (windowSizeClass.widthSizeClass) {
|
val navigationType by remember {
|
||||||
WindowWidthSizeClass.Compact -> {
|
derivedStateOf {
|
||||||
MainNavigationType.BOTTOM_NAVIGATION
|
when (windowWidthSizeClass) {
|
||||||
}
|
WindowWidthSizeClass.Compact -> {
|
||||||
WindowWidthSizeClass.Medium -> {
|
MainNavigationType.BOTTOM_NAVIGATION
|
||||||
MainNavigationType.NAVIGATION_RAIL
|
}
|
||||||
}
|
|
||||||
WindowWidthSizeClass.Expanded -> {
|
WindowWidthSizeClass.Medium -> {
|
||||||
if (foldingDevicePosture is DevicePosture.BookPosture) {
|
MainNavigationType.NAVIGATION_RAIL
|
||||||
MainNavigationType.NAVIGATION_RAIL
|
}
|
||||||
} else {
|
|
||||||
MainNavigationType.PERMANENT_NAVIGATION_DRAWER
|
WindowWidthSizeClass.Expanded -> {
|
||||||
|
if (foldingDevicePosture is DevicePosture.BookPosture) {
|
||||||
|
MainNavigationType.NAVIGATION_RAIL
|
||||||
|
} else {
|
||||||
|
MainNavigationType.PERMANENT_NAVIGATION_DRAWER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
MainNavigationType.BOTTOM_NAVIGATION
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
|
||||||
MainNavigationType.BOTTOM_NAVIGATION
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content inside Navigation Rail/Drawer can also be positioned at top, bottom or center for
|
* Content inside Navigation Rail/Drawer can also be positioned at top, bottom or center for
|
||||||
* ergonomics and reachability depending upon the height of the device.
|
* ergonomics and reachability depending upon the height of the device.
|
||||||
*/
|
*/
|
||||||
val navigationContentPosition = when (windowSizeClass.heightSizeClass) {
|
val navigationContentPosition by remember {
|
||||||
WindowHeightSizeClass.Compact -> {
|
derivedStateOf {
|
||||||
MainNavigationContentPosition.TOP
|
when (windowHeightSizeClass) {
|
||||||
}
|
WindowHeightSizeClass.Compact -> {
|
||||||
|
MainNavigationContentPosition.TOP
|
||||||
|
}
|
||||||
|
|
||||||
WindowHeightSizeClass.Medium,
|
WindowHeightSizeClass.Medium,
|
||||||
WindowHeightSizeClass.Expanded -> {
|
WindowHeightSizeClass.Expanded -> {
|
||||||
MainNavigationContentPosition.CENTER
|
MainNavigationContentPosition.CENTER
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
MainNavigationContentPosition.TOP
|
MainNavigationContentPosition.TOP
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val onReselected: (Int) -> Unit = {
|
val onReselected: (Int) -> Unit = {
|
||||||
coroutineScope.launch {
|
coroutineScope.emitGlobalEvent(
|
||||||
eventFlows[it].emit(MainUiEvent.Refresh)
|
GlobalEvent.Refresh(navigationItems[it].id)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
NavigationWrapper(
|
NavigationWrapper(
|
||||||
currentPosition = pagerState.currentPage,
|
currentPosition = pagerState.currentPage,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -414,6 +415,7 @@ fun BottomNavigation(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class NavigationItem(
|
data class NavigationItem(
|
||||||
val id: String,
|
val id: String,
|
||||||
val icon: @Composable (selected: Boolean) -> ImageVector,
|
val icon: @Composable (selected: Boolean) -> ImageVector,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.main.explore
|
package com.huanchengfly.tieba.post.ui.page.main.explore
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Tab
|
import androidx.compose.material.Tab
|
||||||
|
|
@ -11,6 +13,7 @@ import androidx.compose.material.Text
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -21,10 +24,11 @@ import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.activities.NewSearchActivity
|
import com.huanchengfly.tieba.post.activities.NewSearchActivity
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.goToActivity
|
import com.huanchengfly.tieba.post.goToActivity
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.explore.concern.ConcernPage
|
import com.huanchengfly.tieba.post.ui.page.main.explore.concern.ConcernPage
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.explore.hot.HotPage
|
import com.huanchengfly.tieba.post.ui.page.main.explore.hot.HotPage
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.explore.personalized.PersonalizedPage
|
import com.huanchengfly.tieba.post.ui.page.main.explore.personalized.PersonalizedPage
|
||||||
|
|
@ -34,45 +38,93 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.PagerTabIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
import com.huanchengfly.tieba.post.ui.widgets.compose.accountNavIconIfCompact
|
||||||
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ExplorePageItem(
|
||||||
|
val id: String,
|
||||||
|
val name: @Composable () -> Unit,
|
||||||
|
val content: @Composable () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ExplorePage(
|
private fun ColumnScope.ExplorePageTab(
|
||||||
eventFlow: Flow<MainUiEvent>,
|
pagerState: PagerState,
|
||||||
|
pages: ImmutableList<ExplorePageItem>
|
||||||
) {
|
) {
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
indicator = { tabPositions ->
|
||||||
|
PagerTabIndicator(
|
||||||
|
pagerState = pagerState,
|
||||||
|
tabPositions = tabPositions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
divider = {},
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
contentColor = ExtendedTheme.colors.onTopBar,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.width(76.dp * pages.size),
|
||||||
|
) {
|
||||||
|
pages.forEachIndexed { index, item ->
|
||||||
|
Tab(
|
||||||
|
text = item.name,
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
if (pagerState.currentPage == index) {
|
||||||
|
coroutineScope.emitGlobalEvent(GlobalEvent.Refresh(item.id))
|
||||||
|
} else {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ExplorePage() {
|
||||||
val account = LocalAccount.current
|
val account = LocalAccount.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
val eventFlows = remember {
|
val loggedIn = remember(account) { account != null }
|
||||||
listOf(
|
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
val pages = remember {
|
||||||
MutableSharedFlow<MainUiEvent>(),
|
listOfNotNull(
|
||||||
MutableSharedFlow<MainUiEvent>()
|
if (loggedIn) ExplorePageItem(
|
||||||
)
|
"concern",
|
||||||
|
{ Text(text = stringResource(id = R.string.title_concern)) },
|
||||||
|
{ ConcernPage() }
|
||||||
|
) else null,
|
||||||
|
ExplorePageItem(
|
||||||
|
"personalized",
|
||||||
|
{ Text(text = stringResource(id = R.string.title_personalized)) },
|
||||||
|
{ PersonalizedPage() }
|
||||||
|
),
|
||||||
|
ExplorePageItem(
|
||||||
|
"hot",
|
||||||
|
{ Text(text = stringResource(id = R.string.title_hot)) },
|
||||||
|
{ HotPage() }
|
||||||
|
),
|
||||||
|
).toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val firstIndex = if (account != null) 0 else -1
|
|
||||||
|
|
||||||
val pages = listOfNotNull<Pair<String, (@Composable () -> Unit)>>(
|
|
||||||
if (account != null) stringResource(id = R.string.title_concern) to @Composable {
|
|
||||||
ConcernPage(eventFlows[firstIndex + 0])
|
|
||||||
} else null,
|
|
||||||
stringResource(id = R.string.title_personalized) to @Composable {
|
|
||||||
PersonalizedPage(eventFlows[firstIndex + 1])
|
|
||||||
},
|
|
||||||
stringResource(id = R.string.title_hot) to @Composable {
|
|
||||||
HotPage(eventFlows[firstIndex + 2])
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val pagerState = rememberPagerState(initialPage = if (account != null) 1 else 0)
|
val pagerState = rememberPagerState(initialPage = if (account != null) 1 else 0)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
eventFlow.onEvent<MainUiEvent.Refresh> {
|
onGlobalEvent<GlobalEvent.Refresh>(
|
||||||
eventFlows[pagerState.currentPage].emit(it)
|
filter = { it.key == "explore" }
|
||||||
|
) {
|
||||||
|
coroutineScope.emitGlobalEvent(GlobalEvent.Refresh(pages[pagerState.currentPage].id))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -90,37 +142,7 @@ fun ExplorePage(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
TabRow(
|
ExplorePageTab(pagerState = pagerState, pages = pages)
|
||||||
selectedTabIndex = pagerState.currentPage,
|
|
||||||
indicator = { tabPositions ->
|
|
||||||
PagerTabIndicator(
|
|
||||||
pagerState = pagerState,
|
|
||||||
tabPositions = tabPositions
|
|
||||||
)
|
|
||||||
},
|
|
||||||
divider = {},
|
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
contentColor = ExtendedTheme.colors.onTopBar,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
.width(76.dp * pages.size),
|
|
||||||
) {
|
|
||||||
pages.forEachIndexed { index, pair ->
|
|
||||||
Tab(
|
|
||||||
text = { Text(text = pair.first) },
|
|
||||||
selected = pagerState.currentPage == index,
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
if (pagerState.currentPage == index) {
|
|
||||||
eventFlows[pagerState.currentPage].emit(MainUiEvent.Refresh)
|
|
||||||
} else {
|
|
||||||
pagerState.animateScrollToPage(index)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
@ -129,12 +151,12 @@ fun ExplorePage(
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
pageCount = pages.size,
|
pageCount = pages.size,
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
key = { pages[it].first },
|
key = { pages[it].id },
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
userScrollEnabled = true,
|
userScrollEnabled = true,
|
||||||
) {
|
) {
|
||||||
pages[it].second()
|
pages[it].content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.main.explore.concern
|
package com.huanchengfly.tieba.post.ui.page.main.explore.concern
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
|
@ -20,8 +19,9 @@ import androidx.compose.ui.unit.dp
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.hasAgree
|
import com.huanchengfly.tieba.post.api.models.protos.hasAgree
|
||||||
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
|
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
|
||||||
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
|
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
|
|
@ -29,17 +29,14 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
|
import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ConcernPage(
|
fun ConcernPage(
|
||||||
eventFlow: Flow<MainUiEvent>,
|
|
||||||
viewModel: ConcernViewModel = pageViewModel()
|
viewModel: ConcernViewModel = pageViewModel()
|
||||||
) {
|
) {
|
||||||
LazyLoad(loaded = viewModel.initialized) {
|
LazyLoad(loaded = viewModel.initialized) {
|
||||||
|
|
@ -67,7 +64,9 @@ fun ConcernPage(
|
||||||
refreshing = isRefreshing,
|
refreshing = isRefreshing,
|
||||||
onRefresh = { viewModel.send(ConcernUiIntent.Refresh) })
|
onRefresh = { viewModel.send(ConcernUiIntent.Refresh) })
|
||||||
|
|
||||||
eventFlow.onEvent<MainUiEvent.Refresh> {
|
onGlobalEvent<GlobalEvent.Refresh>(
|
||||||
|
filter = { it.key == "concern" }
|
||||||
|
) {
|
||||||
viewModel.send(ConcernUiIntent.Refresh)
|
viewModel.send(ConcernUiIntent.Refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.huanchengfly.tieba.post.ui.page.main.explore.hot
|
||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -48,9 +47,10 @@ import com.google.accompanist.placeholder.material.placeholder
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
|
import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.hasAgree
|
import com.huanchengfly.tieba.post.api.models.protos.hasAgree
|
||||||
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.OrangeA700
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.OrangeA700
|
||||||
|
|
@ -61,7 +61,6 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.HotTopicListPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
|
import com.huanchengfly.tieba.post.ui.widgets.compose.NetworkImage
|
||||||
|
|
@ -72,20 +71,20 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.items
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.itemsIndexed
|
import com.huanchengfly.tieba.post.ui.widgets.compose.itemsIndexed
|
||||||
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
|
import com.huanchengfly.tieba.post.utils.StringUtil.getShortNumString
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
fun HotPage(
|
fun HotPage(
|
||||||
eventFlow: Flow<MainUiEvent>,
|
|
||||||
viewModel: HotViewModel = pageViewModel()
|
viewModel: HotViewModel = pageViewModel()
|
||||||
) {
|
) {
|
||||||
LazyLoad(loaded = viewModel.initialized) {
|
LazyLoad(loaded = viewModel.initialized) {
|
||||||
viewModel.send(HotUiIntent.Load)
|
viewModel.send(HotUiIntent.Load)
|
||||||
viewModel.initialized = true
|
viewModel.initialized = true
|
||||||
}
|
}
|
||||||
eventFlow.onEvent<MainUiEvent.Refresh> {
|
onGlobalEvent<GlobalEvent.Refresh>(
|
||||||
|
filter = { it.key == "hot" }
|
||||||
|
) {
|
||||||
viewModel.send(HotUiIntent.Load)
|
viewModel.send(HotUiIntent.Load)
|
||||||
}
|
}
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,10 @@ import com.huanchengfly.tieba.post.api.models.protos.personalized.DislikeReason
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
|
import com.huanchengfly.tieba.post.api.models.protos.personalized.ThreadPersonalized
|
||||||
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
|
import com.huanchengfly.tieba.post.arch.BaseComposeActivity
|
||||||
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
|
import com.huanchengfly.tieba.post.arch.CommonUiEvent.ScrollToTop.bindScrollToTopEvent
|
||||||
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
|
|
@ -60,19 +61,16 @@ import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClas
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
import com.huanchengfly.tieba.post.ui.widgets.compose.FeedCard
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoad
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
import com.huanchengfly.tieba.post.ui.widgets.compose.LoadMoreLayout
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
import com.huanchengfly.tieba.post.ui.widgets.compose.VerticalDivider
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PersonalizedPage(
|
fun PersonalizedPage(
|
||||||
eventFlow: Flow<MainUiEvent>,
|
|
||||||
viewModel: PersonalizedViewModel = pageViewModel()
|
viewModel: PersonalizedViewModel = pageViewModel()
|
||||||
) {
|
) {
|
||||||
LazyLoad(loaded = viewModel.initialized) {
|
LazyLoad(loaded = viewModel.initialized) {
|
||||||
|
|
@ -121,13 +119,11 @@ fun PersonalizedPage(
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
eventFlow.onEvent<MainUiEvent.Refresh> {
|
onGlobalEvent<GlobalEvent.Refresh>(
|
||||||
|
filter = { it.key == "personalized" }
|
||||||
|
) {
|
||||||
viewModel.send(PersonalizedUiIntent.Refresh)
|
viewModel.send(PersonalizedUiIntent.Refresh)
|
||||||
}
|
}
|
||||||
viewModel.onEvent<PersonalizedUiEvent.RefreshSuccess> {
|
|
||||||
refreshCount = it.count
|
|
||||||
showRefreshTip = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRefreshTip) {
|
if (showRefreshTip) {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
|
@ -39,7 +40,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -69,15 +69,15 @@ import com.google.accompanist.placeholder.placeholder
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.activities.LoginActivity
|
import com.huanchengfly.tieba.post.activities.LoginActivity
|
||||||
import com.huanchengfly.tieba.post.activities.NewSearchActivity
|
import com.huanchengfly.tieba.post.activities.NewSearchActivity
|
||||||
|
import com.huanchengfly.tieba.post.arch.GlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.onEvent
|
import com.huanchengfly.tieba.post.arch.onGlobalEvent
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
import com.huanchengfly.tieba.post.goToActivity
|
import com.huanchengfly.tieba.post.goToActivity
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
import com.huanchengfly.tieba.post.ui.page.LocalNavigator
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.main.MainUiEvent
|
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.Chip
|
import com.huanchengfly.tieba.post.ui.widgets.Chip
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
import com.huanchengfly.tieba.post.ui.widgets.compose.ActionItem
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar
|
||||||
|
|
@ -96,7 +96,6 @@ import com.huanchengfly.tieba.post.utils.AccountUtil.LocalAccount
|
||||||
import com.huanchengfly.tieba.post.utils.ImageUtil
|
import com.huanchengfly.tieba.post.utils.ImageUtil
|
||||||
import com.huanchengfly.tieba.post.utils.TiebaUtil
|
import com.huanchengfly.tieba.post.utils.TiebaUtil
|
||||||
import com.huanchengfly.tieba.post.utils.appPreferences
|
import com.huanchengfly.tieba.post.utils.appPreferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells {
|
private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells {
|
||||||
return if (listSingle) {
|
return if (listSingle) {
|
||||||
|
|
@ -232,47 +231,22 @@ private fun ForumItemPlaceholder(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ForumItem(
|
private fun ForumItem(
|
||||||
viewModel: HomeViewModel,
|
|
||||||
item: HomeUiState.Forum,
|
item: HomeUiState.Forum,
|
||||||
showAvatar: Boolean,
|
showAvatar: Boolean,
|
||||||
isTopForum: Boolean = false
|
onClick: (HomeUiState.Forum) -> Unit,
|
||||||
|
onUnfollow: (HomeUiState.Forum) -> Unit,
|
||||||
|
onAddTopForum: (HomeUiState.Forum) -> Unit,
|
||||||
|
onDeleteTopForum: (HomeUiState.Forum) -> Unit,
|
||||||
|
isTopForum: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val navigator = LocalNavigator.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val menuState = rememberMenuState()
|
val menuState = rememberMenuState()
|
||||||
var willUnfollow by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
if (willUnfollow) {
|
|
||||||
val dialogState = rememberDialogState()
|
|
||||||
|
|
||||||
ConfirmDialog(
|
|
||||||
dialogState = dialogState,
|
|
||||||
onConfirm = { viewModel.send(HomeUiIntent.Unfollow(item.forumId, item.forumName)) },
|
|
||||||
modifier = Modifier,
|
|
||||||
onDismiss = {
|
|
||||||
willUnfollow = false
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
id = R.string.title_dialog_unfollow_forum,
|
|
||||||
item.forumName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = "launchUnfollowDialog") {
|
|
||||||
dialogState.show = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LongClickMenu(
|
LongClickMenu(
|
||||||
menuContent = {
|
menuContent = {
|
||||||
if (isTopForum) {
|
if (isTopForum) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.send(HomeUiIntent.TopForums.Delete(item.forumId))
|
onDeleteTopForum(item)
|
||||||
menuState.expanded = false
|
menuState.expanded = false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -281,7 +255,7 @@ private fun ForumItem(
|
||||||
} else {
|
} else {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.send(HomeUiIntent.TopForums.Add(item))
|
onAddTopForum(item)
|
||||||
menuState.expanded = false
|
menuState.expanded = false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -298,7 +272,7 @@ private fun ForumItem(
|
||||||
}
|
}
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
willUnfollow = true
|
onUnfollow(item)
|
||||||
menuState.expanded = false
|
menuState.expanded = false
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
@ -307,7 +281,7 @@ private fun ForumItem(
|
||||||
},
|
},
|
||||||
menuState = menuState,
|
menuState = menuState,
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(ForumPageDestination(item.forumName))
|
onClick(item)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -374,7 +348,6 @@ private fun ForumItem(
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
eventFlow: Flow<MainUiEvent>,
|
|
||||||
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
|
viewModel: HomeViewModel = pageViewModel<HomeUiIntent, HomeViewModel>(
|
||||||
listOf(
|
listOf(
|
||||||
HomeUiIntent.Refresh
|
HomeUiIntent.Refresh
|
||||||
|
|
@ -385,6 +358,7 @@ fun HomePage(
|
||||||
) {
|
) {
|
||||||
val account = LocalAccount.current
|
val account = LocalAccount.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.current
|
||||||
val isLoading by viewModel.uiState.collectPartialAsState(
|
val isLoading by viewModel.uiState.collectPartialAsState(
|
||||||
prop1 = HomeUiState::isLoading,
|
prop1 = HomeUiState::isLoading,
|
||||||
initial = true
|
initial = true
|
||||||
|
|
@ -401,14 +375,37 @@ fun HomePage(
|
||||||
prop1 = HomeUiState::error,
|
prop1 = HomeUiState::error,
|
||||||
initial = null
|
initial = null
|
||||||
)
|
)
|
||||||
val isError by remember { derivedStateOf { error != null } }
|
val isLoggedIn = remember(account) { account != null }
|
||||||
|
val isEmpty by remember { derivedStateOf { forums.isEmpty() } }
|
||||||
|
val hasTopForum by remember { derivedStateOf { topForums.isNotEmpty() } }
|
||||||
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
|
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
|
||||||
|
val isError by remember { derivedStateOf { error != null } }
|
||||||
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
|
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
|
||||||
|
|
||||||
eventFlow.onEvent<MainUiEvent.Refresh> {
|
onGlobalEvent<GlobalEvent.Refresh>(
|
||||||
|
filter = { it.key == "home" }
|
||||||
|
) {
|
||||||
viewModel.send(HomeUiIntent.Refresh)
|
viewModel.send(HomeUiIntent.Refresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unfollowForum by remember { mutableStateOf<HomeUiState.Forum?>(null) }
|
||||||
|
val confirmUnfollowDialog = rememberDialogState()
|
||||||
|
ConfirmDialog(
|
||||||
|
dialogState = confirmUnfollowDialog,
|
||||||
|
onConfirm = {
|
||||||
|
unfollowForum?.let {
|
||||||
|
viewModel.send(HomeUiIntent.Unfollow(it.forumId, it.forumName))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.title_dialog_unfollow_forum,
|
||||||
|
unfollowForum?.forumName.orEmpty()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
backgroundColor = Color.Transparent,
|
backgroundColor = Color.Transparent,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -435,7 +432,7 @@ fun HomePage(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) { contentPaddings ->
|
) { contentPaddings ->
|
||||||
StateScreen(
|
StateScreen(
|
||||||
isEmpty = forums.isEmpty(),
|
isEmpty = isEmpty,
|
||||||
isError = isError,
|
isError = isError,
|
||||||
isLoading = isLoading,
|
isLoading = isLoading,
|
||||||
modifier = Modifier.padding(contentPaddings),
|
modifier = Modifier.padding(contentPaddings),
|
||||||
|
|
@ -444,7 +441,7 @@ fun HomePage(
|
||||||
},
|
},
|
||||||
emptyScreen = {
|
emptyScreen = {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
loggedIn = account != null,
|
loggedIn = isLoggedIn,
|
||||||
canOpenExplore = canOpenExplore,
|
canOpenExplore = canOpenExplore,
|
||||||
onOpenExplore = onOpenExplore
|
onOpenExplore = onOpenExplore
|
||||||
)
|
)
|
||||||
|
|
@ -458,7 +455,8 @@ fun HomePage(
|
||||||
) {
|
) {
|
||||||
val pullRefreshState = rememberPullRefreshState(
|
val pullRefreshState = rememberPullRefreshState(
|
||||||
refreshing = isLoading,
|
refreshing = isLoading,
|
||||||
onRefresh = { viewModel.send(HomeUiIntent.Refresh) })
|
onRefresh = { viewModel.send(HomeUiIntent.Refresh) }
|
||||||
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.pullRefresh(pullRefreshState)
|
.pullRefresh(pullRefreshState)
|
||||||
|
|
@ -468,15 +466,14 @@ fun HomePage(
|
||||||
state = gridState,
|
state = gridState,
|
||||||
columns = gridCells,
|
columns = gridCells,
|
||||||
contentPadding = PaddingValues(bottom = 12.dp),
|
contentPadding = PaddingValues(bottom = 12.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize(),
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
) {
|
||||||
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
|
item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) {
|
||||||
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
|
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||||
context.goToActivity<NewSearchActivity>()
|
context.goToActivity<NewSearchActivity>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (topForums.isNotEmpty()) {
|
if (hasTopForum) {
|
||||||
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
|
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
|
||||||
Column {
|
Column {
|
||||||
Header(
|
Header(
|
||||||
|
|
@ -486,9 +483,28 @@ fun HomePage(
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(count = topForums.size, key = { "Top${topForums[it].forumId}" }) {
|
items(
|
||||||
val item = topForums[it]
|
items = topForums,
|
||||||
ForumItem(viewModel, item, listSingle, true)
|
key = { "Top${it.forumId}" }
|
||||||
|
) { item ->
|
||||||
|
ForumItem(
|
||||||
|
item,
|
||||||
|
listSingle,
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(ForumPageDestination(it.forumName))
|
||||||
|
},
|
||||||
|
onUnfollow = {
|
||||||
|
unfollowForum = it
|
||||||
|
confirmUnfollowDialog.show()
|
||||||
|
},
|
||||||
|
onAddTopForum = {
|
||||||
|
viewModel.send(HomeUiIntent.TopForums.Add(it))
|
||||||
|
},
|
||||||
|
onDeleteTopForum = {
|
||||||
|
viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId))
|
||||||
|
},
|
||||||
|
isTopForum = true
|
||||||
|
)
|
||||||
}
|
}
|
||||||
item(
|
item(
|
||||||
key = "Spacer",
|
key = "Spacer",
|
||||||
|
|
@ -506,9 +522,27 @@ fun HomePage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(count = forums.size, key = { forums[it].forumId }) {
|
items(
|
||||||
val item = forums[it]
|
items = forums,
|
||||||
ForumItem(viewModel, item, listSingle)
|
key = { it.forumId }
|
||||||
|
) { item ->
|
||||||
|
ForumItem(
|
||||||
|
item,
|
||||||
|
listSingle,
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(ForumPageDestination(it.forumName))
|
||||||
|
},
|
||||||
|
onUnfollow = {
|
||||||
|
unfollowForum = it
|
||||||
|
confirmUnfollowDialog.show()
|
||||||
|
},
|
||||||
|
onAddTopForum = {
|
||||||
|
viewModel.send(HomeUiIntent.TopForums.Add(it))
|
||||||
|
},
|
||||||
|
onDeleteTopForum = {
|
||||||
|
viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId))
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.main.home
|
package com.huanchengfly.tieba.post.ui.page.main.home
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import com.huanchengfly.tieba.post.api.TiebaApi
|
import com.huanchengfly.tieba.post.api.TiebaApi
|
||||||
import com.huanchengfly.tieba.post.api.models.CommonResponse
|
import com.huanchengfly.tieba.post.api.models.CommonResponse
|
||||||
import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse
|
import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse
|
||||||
|
|
@ -12,6 +14,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import org.litepal.LitePal
|
import org.litepal.LitePal
|
||||||
|
|
||||||
|
@Stable
|
||||||
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
|
class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState, HomeUiEvent>() {
|
||||||
override fun createInitialState(): HomeUiState = HomeUiState()
|
override fun createInitialState(): HomeUiState = HomeUiState()
|
||||||
|
|
||||||
|
|
@ -184,12 +187,14 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
data class HomeUiState(
|
data class HomeUiState(
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val forums: List<Forum> = emptyList(),
|
val forums: List<Forum> = emptyList(),
|
||||||
val topForums: List<Forum> = emptyList(),
|
val topForums: List<Forum> = emptyList(),
|
||||||
val error: Throwable? = null,
|
val error: Throwable? = null,
|
||||||
) : UiState {
|
) : UiState {
|
||||||
|
@Immutable
|
||||||
data class Forum(
|
data class Forum(
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val forumId: String,
|
val forumId: String,
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ fun ClickMenu(
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LongClickMenu(
|
fun LongClickMenu(
|
||||||
menuContent: @Composable() (ColumnScope.() -> Unit),
|
menuContent: @Composable (ColumnScope.() -> Unit),
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
menuState: MenuState = rememberMenuState(),
|
menuState: MenuState = rememberMenuState(),
|
||||||
onClick: (() -> Unit)? = null,
|
onClick: (() -> Unit)? = null,
|
||||||
|
|
|
||||||
|
|
@ -706,4 +706,5 @@
|
||||||
<string name="above_is_latest_post">以上为最新回复</string>
|
<string name="above_is_latest_post">以上为最新回复</string>
|
||||||
<string name="below_is_latest_post">以下为最新回复</string>
|
<string name="below_is_latest_post">以下为最新回复</string>
|
||||||
<string name="btn_open_origin_thread">查看原贴</string>
|
<string name="btn_open_origin_thread">查看原贴</string>
|
||||||
|
<string name="tip_blocked_thread">由于你的屏蔽设置,该贴已被屏蔽</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue