diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/models/ThreadStoreBean.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/models/ThreadStoreBean.kt index b1ddc3f3..ecff9c9e 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/models/ThreadStoreBean.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/models/ThreadStoreBean.kt @@ -2,6 +2,7 @@ package com.huanchengfly.tieba.post.api.models import com.google.gson.annotations.SerializedName import com.huanchengfly.tieba.post.models.BaseBean +import javax.annotation.concurrent.Immutable data class ThreadStoreBean( @SerializedName("error_code") @@ -10,6 +11,7 @@ data class ThreadStoreBean( @SerializedName("store_thread") val storeThread: List? = null ) : BaseBean() { + @Immutable data class ThreadStoreInfo( @SerializedName("thread_id") val threadId: String, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/arch/Extensions.kt b/app/src/main/java/com/huanchengfly/tieba/post/arch/Extensions.kt index d4fc4e66..65767b53 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/arch/Extensions.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/arch/Extensions.kt @@ -54,6 +54,7 @@ fun Flow.collectPartialAsState( this@collectPartialAsState .map { prop1.get(it) } .distinctUntilChanged() + .flowOn(Dispatchers.IO) .collect { value = it } @@ -71,6 +72,7 @@ inline fun Flow.onEvent( this@onEvent .filterIsInstance() .cancellable() + .flowOn(Dispatchers.IO) .collect { launch { listener(it) @@ -95,6 +97,7 @@ inline fun BaseViewModel<*, *, *, *>.onEvent( uiEventFlow .filterIsInstance() .cancellable() + .flowOn(Dispatchers.IO) .collect { coroutineScope.launch { listener(it) @@ -119,6 +122,7 @@ inline fun > pageViewModel(): VM { uiEventFlow .filterIsInstance() .cancellable() + .flowOn(Dispatchers.IO) .collectIn(context) { context.handleCommonEvent(it) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/common/prefs/widgets/ListPref.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/common/prefs/widgets/ListPref.kt index cad81606..4b1baed5 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/common/prefs/widgets/ListPref.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/common/prefs/widgets/ListPref.kt @@ -17,6 +17,8 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Dialog import com.huanchengfly.tieba.post.ui.widgets.compose.DialogNegativeButton import com.huanchengfly.tieba.post.ui.widgets.compose.picker.ListSinglePicker import com.huanchengfly.tieba.post.ui.widgets.compose.rememberDialogState +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.launch /** @@ -92,6 +94,9 @@ fun ListPref( onClick = { if (enabled) dialogState.show() }, ) + val itemTitle = remember(entries) { entries.map { it.value }.toImmutableList() } + val itemValues = remember(entries) { entries.map { it.key }.toImmutableList() } + Dialog( dialogState = dialogState, title = { Text(text = title) }, @@ -100,15 +105,15 @@ fun ListPref( } ) { ListSinglePicker( - itemTitles = entries.map { it.value }, - itemValues = entries.map { it.key }, + itemTitles = itemTitle, + itemValues = itemValues, selectedPosition = entries.keys.indexOf(selected), onItemSelected = { _, title, value, _ -> edit(current = value to title) dismiss() }, - itemIcons = icons, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp), + itemIcons = icons.toImmutableMap() ) } // if (showDialog) { diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/HistoryPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/HistoryPage.kt index 6b5d5a18..9067bdc9 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/HistoryPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/HistoryPage.kt @@ -14,7 +14,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.rememberScaffoldState import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -24,6 +23,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.arch.emitGlobalEvent import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.page.ProvideNavigator import com.huanchengfly.tieba.post.ui.page.history.list.HistoryListPage @@ -36,7 +36,6 @@ import com.huanchengfly.tieba.post.utils.HistoryUtil import com.ramcosta.composedestinations.annotation.DeepLink import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @@ -55,8 +54,6 @@ fun HistoryPage( val context = LocalContext.current - val eventFlow = remember { MutableSharedFlow() } - MyScaffold( backgroundColor = Color.Transparent, scaffoldState = scaffoldState, @@ -70,7 +67,7 @@ fun HistoryPage( IconButton(onClick = { coroutineScope.launch { HistoryUtil.deleteAll() - eventFlow.emit(HistoryListUiEvent.DeleteAll) + emitGlobalEvent(HistoryListUiEvent.DeleteAll) launch { scaffoldState.snackbarHostState.showSnackbar( context.getString( @@ -148,9 +145,9 @@ fun HistoryPage( userScrollEnabled = true, ) { if (it == 0) { - HistoryListPage(type = HistoryUtil.TYPE_THREAD, eventFlow = eventFlow) + HistoryListPage(type = HistoryUtil.TYPE_THREAD) } else { - HistoryListPage(type = HistoryUtil.TYPE_FORUM, eventFlow = eventFlow) + HistoryListPage(type = HistoryUtil.TYPE_FORUM) } } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/list/HistoryListPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/list/HistoryListPage.kt index 79b115f9..6437cd2d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/list/HistoryListPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/history/list/HistoryListPage.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.DropdownMenuItem import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -25,6 +24,7 @@ import com.huanchengfly.tieba.post.activities.ForumActivity import com.huanchengfly.tieba.post.activities.ThreadActivity 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.fromJson import com.huanchengfly.tieba.post.models.ThreadHistoryInfoBean @@ -45,29 +45,19 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState import com.huanchengfly.tieba.post.utils.DateTimeUtils import com.huanchengfly.tieba.post.utils.HistoryUtil -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun HistoryListPage( type: Int, - eventFlow: Flow, viewModel: HistoryListViewModel = if (type == HistoryUtil.TYPE_THREAD) pageViewModel() else pageViewModel() ) { LazyLoad(loaded = viewModel.initialized) { viewModel.send(HistoryListUiIntent.Refresh) viewModel.initialized = true } - LaunchedEffect(null) { - launch { - eventFlow - .filterIsInstance() - .collect { - viewModel.send(HistoryListUiIntent.DeleteAll) - } - } + onGlobalEvent { + viewModel.send(HistoryListUiIntent.DeleteAll) } val isLoadingMore by viewModel.uiState.collectPartialAsState( prop1 = HistoryListUiState::isLoadingMore, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt index 4ba803f2..60ef031f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/explore/personalized/PersonalizedPage.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -32,27 +33,28 @@ 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.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.github.panpf.sketch.request.PauseLoadWhenScrollingDrawableDecodeInterceptor import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.api.models.protos.ThreadInfo 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.BaseComposeActivity.Companion.LocalWindowSizeClass 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 @@ -124,6 +126,10 @@ fun PersonalizedPage( ) { viewModel.send(PersonalizedUiIntent.Refresh) } + viewModel.onEvent { + refreshCount = it.count + showRefreshTip = true + } if (showRefreshTip) { LaunchedEffect(Unit) { @@ -135,14 +141,14 @@ fun PersonalizedPage( showRefreshTip = false } } - if (lazyListState.isScrollInProgress) { - DisposableEffect(Unit) { - PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true - onDispose { - PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false - } - } - } +// if (lazyListState.isScrollInProgress) { +// DisposableEffect(Unit) { +// PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = true +// onDispose { +// PauseLoadWhenScrollingDrawableDecodeInterceptor.scrolling = false +// } +// } +// } Box(modifier = Modifier.pullRefresh(pullRefreshState)) { LoadMoreLayout( isLoading = isLoadingMore, @@ -150,6 +156,7 @@ fun PersonalizedPage( onLoadMore = { viewModel.send(PersonalizedUiIntent.LoadMore(currentPage + 1)) }, ) { FeedList( + state = lazyListState, dataProvider = { data }, personalizedDataProvider = { threadPersonalizedData }, refreshPositionProvider = { refreshPosition }, @@ -191,12 +198,10 @@ fun PersonalizedPage( ) ) }, - onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) }, - onOpenForum = { - navigator.navigate(ForumPageDestination(it)) - }, - state = lazyListState - ) + onRefresh = { viewModel.send(PersonalizedUiIntent.Refresh) } + ) { + navigator.navigate(ForumPageDestination(it)) + } } PullRefreshIndicator( @@ -213,28 +218,34 @@ fun PersonalizedPage( exit = slideOutVertically() + fadeOut(), modifier = Modifier.align(Alignment.TopCenter) ) { - Box( - modifier = Modifier - .padding(top = 72.dp) - .clip(RoundedCornerShape(100)) - .background( - color = ExtendedTheme.colors.primary, - shape = RoundedCornerShape(100) - ) - .padding(horizontal = 16.dp, vertical = 8.dp) - .align(Alignment.TopCenter) - ) { - Text( - text = stringResource(id = R.string.toast_feed_refresh, refreshCount), - color = ExtendedTheme.colors.onAccent - ) - } + RefreshTip(refreshCount = refreshCount) } } } +@Composable +private fun BoxScope.RefreshTip(refreshCount: Int) { + Box( + modifier = Modifier + .padding(top = 72.dp) + .clip(RoundedCornerShape(100)) + .background( + color = ExtendedTheme.colors.primary, + shape = RoundedCornerShape(100) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + .align(Alignment.TopCenter) + ) { + Text( + text = stringResource(id = R.string.toast_feed_refresh, refreshCount), + color = ExtendedTheme.colors.onAccent + ) + } +} + @Composable private fun FeedList( + state: LazyListState, dataProvider: () -> List>, personalizedDataProvider: () -> List?>, refreshPositionProvider: () -> Int, @@ -245,16 +256,19 @@ private fun FeedList( onDislike: (ThreadInfo, Long, List>) -> Unit, onRefresh: () -> Unit, onOpenForum: (forumName: String) -> Unit = {}, - state: LazyListState, ) { val data = dataProvider() val threadPersonalizedData = personalizedDataProvider() val refreshPosition = refreshPositionProvider() val hiddenThreadIds = hiddenThreadIdsProvider() - val windowSizeClass = BaseComposeActivity.LocalWindowSizeClass.current - val itemFraction = when (windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Expanded -> 0.5f - else -> 1f + val windowWidthSizeClass by rememberUpdatedState(newValue = LocalWindowSizeClass.current.widthSizeClass) + val itemFraction by remember { + derivedStateOf { + when (windowWidthSizeClass) { + WindowWidthSizeClass.Expanded -> 0.5f + else -> 1f + } + } } LazyColumn( state = state, @@ -273,11 +287,23 @@ private fun FeedList( } } ) { index, item -> + val isHidden = + remember(hiddenThreadIds, item) { hiddenThreadIds.contains(item.get { threadId }) } + val personalized = + remember(threadPersonalizedData, index) { threadPersonalizedData.getOrNull(index) } + val isRefreshPosition = + remember(index, refreshPosition) { index + 1 == refreshPosition } + val isNotLast = remember(index, data.size) { index < data.size - 1 } + val showDivider = remember( + isHidden, + isRefreshPosition, + isNotLast + ) { !isHidden && !isRefreshPosition && isNotLast } Column( modifier = Modifier.fillMaxWidth(itemFraction) ) { AnimatedVisibility( - visible = !hiddenThreadIds.contains(item.get { threadId }), + visible = !isHidden, enter = EnterTransition.None, exit = shrinkVertically() + fadeOut() ) { @@ -290,8 +316,6 @@ private fun FeedList( onOpenForum(it.name) } ) { - val personalized = threadPersonalizedData.getOrNull(index) - if (personalized != null) { Dislike( personalized = personalized, @@ -302,35 +326,40 @@ private fun FeedList( } } } - if (!hiddenThreadIds.contains(item.get { threadId })) { - if ((refreshPosition == 0 || index + 1 != refreshPosition) && index < data.size - 1) { - VerticalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - thickness = 2.dp - ) - } + if (showDivider) { + VerticalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 2.dp + ) } - if (refreshPosition != 0 && index + 1 == refreshPosition) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onRefresh) - .padding(8.dp), - ) { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - Spacer(modifier = Modifier.width(16.dp)) - Text( - text = stringResource(id = R.string.tip_refresh), - style = MaterialTheme.typography.subtitle1 - ) - } + if (isRefreshPosition) { + RefreshTip(onRefresh) } } } } +} + +@Composable +private fun RefreshTip( + onRefresh: () -> Unit +) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onRefresh) + .padding(8.dp), + ) { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = stringResource(id = R.string.tip_refresh), + style = MaterialTheme.typography.subtitle1 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt index 9cf78fcc..ad77ceef 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomePage.kt @@ -2,7 +2,6 @@ package com.huanchengfly.tieba.post.ui.page.main.home import android.content.Context import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -22,7 +21,6 @@ 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 import androidx.compose.material.DropdownMenuItem @@ -85,6 +83,7 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.Button import com.huanchengfly.tieba.post.ui.widgets.compose.ConfirmDialog import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen import com.huanchengfly.tieba.post.ui.widgets.compose.LongClickMenu +import com.huanchengfly.tieba.post.ui.widgets.compose.MenuState import com.huanchengfly.tieba.post.ui.widgets.compose.TextButton import com.huanchengfly.tieba.post.ui.widgets.compose.TipScreen import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar @@ -96,6 +95,7 @@ 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.collections.immutable.persistentListOf private fun getGridCells(context: Context, listSingle: Boolean = context.appPreferences.listSingle): GridCells { return if (listSingle) { @@ -229,6 +229,112 @@ private fun ForumItemPlaceholder( } } +@Composable +private fun ForumItemMenuContent( + menuState: MenuState, + isTopForum: Boolean, + onDeleteTopForum: () -> Unit, + onAddTopForum: () -> Unit, + onCopyName: () -> Unit, + onUnfollow: () -> Unit, +) { + DropdownMenuItem( + onClick = { + if (isTopForum) { + onDeleteTopForum() + } else { + onAddTopForum() + } + menuState.expanded = false + } + ) { + if (isTopForum) { + Text(text = stringResource(id = R.string.menu_top_del)) + } else { + Text(text = stringResource(id = R.string.menu_top)) + } + } + DropdownMenuItem( + onClick = { + onCopyName() + menuState.expanded = false + } + ) { + Text(text = stringResource(id = R.string.title_copy_forum_name)) + } + DropdownMenuItem( + onClick = { + onUnfollow() + menuState.expanded = false + } + ) { + Text(text = stringResource(id = R.string.button_unfollow)) + } +} + +@Composable +private fun ForumItemContent( + item: HomeUiState.Forum, + showAvatar: Boolean +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + AnimatedVisibility(visible = showAvatar) { + Row { + Avatar(data = item.avatar, size = 40.dp, contentDescription = null) + Spacer(modifier = Modifier.width(14.dp)) + } + } + Text( + color = ExtendedTheme.colors.text, + text = item.forumName, + modifier = Modifier + .weight(1f) + .align(CenterVertically), + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + ) + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier + .width(54.dp) + .background( + color = ExtendedTheme.colors.chip, + shape = RoundedCornerShape(3.dp) + ) + .padding(vertical = 4.dp) + .align(CenterVertically) + ) { + Row( + modifier = Modifier.align(Center), + ) { + Text( + text = "Lv.${item.levelId}", + color = ExtendedTheme.colors.onChip, + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(CenterVertically) + ) + if (item.isSign) { + Spacer(modifier = Modifier.width(4.dp)) + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = stringResource(id = R.string.tip_signed), + modifier = Modifier + .size(12.dp) + .align(CenterVertically), + tint = ExtendedTheme.colors.onChip + ) + } + } + } + } +} + @Composable private fun ForumItem( item: HomeUiState.Forum, @@ -243,116 +349,30 @@ private fun ForumItem( val menuState = rememberMenuState() LongClickMenu( menuContent = { - if (isTopForum) { - DropdownMenuItem( - onClick = { - onDeleteTopForum(item) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.menu_top_del)) - } - } else { - DropdownMenuItem( - onClick = { - onAddTopForum(item) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.menu_top)) - } - } - DropdownMenuItem( - onClick = { + ForumItemMenuContent( + menuState = menuState, + isTopForum = isTopForum, + onDeleteTopForum = { onDeleteTopForum(item) }, + onAddTopForum = { onAddTopForum(item) }, + onCopyName = { TiebaUtil.copyText(context, item.forumName) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.title_copy_forum_name)) - } - DropdownMenuItem( - onClick = { - onUnfollow(item) - menuState.expanded = false - } - ) { - Text(text = stringResource(id = R.string.button_unfollow)) - } + }, + onUnfollow = { onUnfollow(item) } + ) }, menuState = menuState, onClick = { onClick(item) } ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(vertical = 12.dp) - .animateContentSize(), - ) { - AnimatedVisibility(visible = showAvatar) { - Row { - Avatar(data = item.avatar, size = 40.dp, contentDescription = null) - Spacer(modifier = Modifier.width(14.dp)) - } - } - Text( - color = ExtendedTheme.colors.text, - text = item.forumName, - modifier = Modifier - .weight(1f) - .align(CenterVertically), - fontSize = 15.sp, - fontWeight = FontWeight.Bold, - maxLines = 1, - ) - Spacer(modifier = Modifier.width(8.dp)) - Box( - modifier = Modifier - .width(54.dp) - .background( - color = ExtendedTheme.colors.chip, - shape = RoundedCornerShape(3.dp) - ) - .padding(vertical = 4.dp) - .align(CenterVertically) - ) { - Row( - modifier = Modifier.align(Center), - ) { - Text( - text = "Lv.${item.levelId}", - color = ExtendedTheme.colors.onChip, - fontSize = 11.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.align(CenterVertically) - ) - if (item.isSign) { - Spacer(modifier = Modifier.width(4.dp)) - Icon( - imageVector = Icons.Rounded.Check, - contentDescription = stringResource(id = R.string.tip_signed), - modifier = Modifier - .size(12.dp) - .align(CenterVertically), - tint = ExtendedTheme.colors.onChip - ) - } - } - } - } + ForumItemContent(item = item, showAvatar = showAvatar) } } @OptIn(ExperimentalMaterialApi::class) @Composable fun HomePage( - viewModel: HomeViewModel = pageViewModel( - listOf( - HomeUiIntent.Refresh - ) - ), + viewModel: HomeViewModel = pageViewModel(listOf(HomeUiIntent.Refresh)), canOpenExplore: Boolean = false, onOpenExplore: () -> Unit = {}, ) { @@ -365,11 +385,11 @@ fun HomePage( ) val forums by viewModel.uiState.collectPartialAsState( prop1 = HomeUiState::forums, - initial = emptyList() + initial = persistentListOf() ) val topForums by viewModel.uiState.collectPartialAsState( prop1 = HomeUiState::topForums, - initial = emptyList() + initial = persistentListOf() ) val error by viewModel.uiState.collectPartialAsState( prop1 = HomeUiState::error, @@ -431,61 +451,92 @@ fun HomePage( }, modifier = Modifier.fillMaxSize(), ) { contentPaddings -> - StateScreen( - isEmpty = isEmpty, - isError = isError, - isLoading = isLoading, - modifier = Modifier.padding(contentPaddings), - onReload = { - viewModel.send(HomeUiIntent.Refresh) - }, - emptyScreen = { - EmptyScreen( - loggedIn = isLoggedIn, - canOpenExplore = canOpenExplore, - onOpenExplore = onOpenExplore - ) - }, - loadingScreen = { - HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells) - }, - errorScreen = { - error?.let { ErrorScreen(error = it) } - } + val pullRefreshState = rememberPullRefreshState( + refreshing = isLoading, + onRefresh = { viewModel.send(HomeUiIntent.Refresh) } + ) + Box( + modifier = Modifier + .pullRefresh(pullRefreshState) + .padding(contentPaddings) ) { - val pullRefreshState = rememberPullRefreshState( - refreshing = isLoading, - onRefresh = { viewModel.send(HomeUiIntent.Refresh) } - ) - Box( - modifier = Modifier - .pullRefresh(pullRefreshState) - ) { - val gridState = rememberLazyGridState() - LazyVerticalGrid( - state = gridState, - columns = gridCells, - contentPadding = PaddingValues(bottom = 12.dp), - modifier = Modifier.fillMaxSize(), - ) { - item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) { - SearchBox(modifier = Modifier.padding(bottom = 12.dp)) { - context.goToActivity() - } + Column { + SearchBox(modifier = Modifier.padding(bottom = 12.dp)) { + context.goToActivity() + } + StateScreen( + isEmpty = isEmpty, + isError = isError, + isLoading = isLoading, + modifier = Modifier.weight(1f), + onReload = { + viewModel.send(HomeUiIntent.Refresh) + }, + emptyScreen = { + EmptyScreen( + loggedIn = isLoggedIn, + canOpenExplore = canOpenExplore, + onOpenExplore = onOpenExplore + ) + }, + loadingScreen = { + HomePageSkeletonScreen(listSingle = listSingle, gridCells = gridCells) + }, + errorScreen = { + error?.let { ErrorScreen(error = it) } } - if (hasTopForum) { - item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) { - Column { - Header( - text = stringResource(id = R.string.title_top_forum), - invert = true + ) { + LazyVerticalGrid( + columns = gridCells, + contentPadding = PaddingValues(bottom = 12.dp), + modifier = Modifier.fillMaxSize(), + ) { + if (hasTopForum) { + item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) { + Column { + Header( + text = stringResource(id = R.string.title_top_forum), + invert = true + ) + Spacer(modifier = Modifier.height(8.dp)) + } + } + 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 ) - Spacer(modifier = Modifier.height(8.dp)) + } + item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) { + Column( + modifier = Modifier.padding( + vertical = 8.dp + ) + ) { + Header(text = stringResource(id = R.string.forum_list_title)) + } } } items( - items = topForums, - key = { "Top${it.forumId}" } + items = forums, + key = { it.forumId } ) { item -> ForumItem( item, @@ -502,58 +553,20 @@ fun HomePage( }, onDeleteTopForum = { viewModel.send(HomeUiIntent.TopForums.Delete(it.forumId)) - }, - isTopForum = true + } ) } - item( - key = "Spacer", - span = { GridItemSpan(maxLineSpan) }) { - Spacer( - modifier = Modifier.height( - 16.dp - ) - ) - } - item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) { - Column { - Header(text = stringResource(id = R.string.forum_list_title)) - Spacer(modifier = Modifier.height(8.dp)) - } - } - } - 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)) - } - ) } } - - PullRefreshIndicator( - refreshing = isLoading, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - backgroundColor = ExtendedTheme.colors.pullRefreshIndicator, - contentColor = ExtendedTheme.colors.primary, - ) } + + PullRefreshIndicator( + refreshing = isLoading, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = ExtendedTheme.colors.pullRefreshIndicator, + contentColor = ExtendedTheme.colors.primary, + ) } } } @@ -570,11 +583,6 @@ private fun HomePageSkeletonScreen( modifier = Modifier .fillMaxSize(), ) { - item(key = "SearchBox", span = { GridItemSpan(maxLineSpan) }) { - SearchBox(modifier = Modifier.padding(bottom = 12.dp)) { - context.goToActivity() - } - } item(key = "TopForumHeaderPlaceholder", span = { GridItemSpan(maxLineSpan) }) { Column { Header( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt index 947aa2a4..8f74ff15 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/home/HomeViewModel.kt @@ -6,12 +6,29 @@ 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 import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage -import com.huanchengfly.tieba.post.arch.* +import com.huanchengfly.tieba.post.arch.BaseViewModel +import com.huanchengfly.tieba.post.arch.CommonUiEvent +import com.huanchengfly.tieba.post.arch.PartialChange +import com.huanchengfly.tieba.post.arch.PartialChangeProducer +import com.huanchengfly.tieba.post.arch.UiEvent +import com.huanchengfly.tieba.post.arch.UiIntent +import com.huanchengfly.tieba.post.arch.UiState import com.huanchengfly.tieba.post.models.database.TopForum import com.huanchengfly.tieba.post.utils.AccountUtil +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import org.litepal.LitePal @Stable @@ -114,8 +131,10 @@ sealed interface HomePartialChange : PartialChange { when (this) { is Success -> { oldState.copy( - forums = oldState.forums.filterNot { it.forumId == forumId }, - topForums = oldState.topForums.filterNot { it.forumId == forumId }, + forums = oldState.forums.filterNot { it.forumId == forumId } + .toImmutableList(), + topForums = oldState.topForums.filterNot { it.forumId == forumId } + .toImmutableList(), ) } @@ -132,8 +151,8 @@ sealed interface HomePartialChange : PartialChange { when (this) { is Success -> oldState.copy( isLoading = false, - forums = forums, - topForums = topForums, + forums = forums.toImmutableList(), + topForums = topForums.toImmutableList(), error = null ) @@ -157,7 +176,9 @@ sealed interface HomePartialChange : PartialChange { sealed interface Delete : HomePartialChange { override fun reduce(oldState: HomeUiState): HomeUiState = when (this) { - is Success -> oldState.copy(topForums = oldState.topForums.filterNot { it.forumId == forumId }) + is Success -> oldState.copy(topForums = oldState.topForums.filterNot { it.forumId == forumId } + .toImmutableList()) + is Failure -> oldState } @@ -174,6 +195,7 @@ sealed interface HomePartialChange : PartialChange { topForumsId.add(forum.forumId) oldState.copy( topForums = oldState.forums.filter { topForumsId.contains(it.forumId) } + .toImmutableList() ) } @@ -190,8 +212,8 @@ sealed interface HomePartialChange : PartialChange { @Immutable data class HomeUiState( val isLoading: Boolean = true, - val forums: List = emptyList(), - val topForums: List = emptyList(), + val forums: ImmutableList = persistentListOf(), + val topForums: ImmutableList = persistentListOf(), val error: Throwable? = null, ) : UiState { @Immutable diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt index df03a790..597f85b7 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/subposts/SubPostsPage.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity @@ -595,7 +596,7 @@ private fun SubPostItem( .padding(start = Sizes.Small + 8.dp) .fillMaxWidth() ) { - contentRenders.forEach { it.Render() } + contentRenders.fastForEach { it.Render() } } } ) diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt index 23995c4e..b1c8f92d 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt @@ -84,6 +84,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastForEachIndexed import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity @@ -1683,7 +1685,7 @@ fun PostCard( ) } - contentRenders.forEach { it.Render() } + contentRenders.fastForEach { it.Render() } } if (showSubPosts && post.sub_post_number > 0 && subPostContents.isNotEmpty() && !immersiveMode) { @@ -1696,7 +1698,7 @@ fun PostCard( .padding(vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(2.dp) ) { - subPostContents.forEachIndexed { index, text -> + subPostContents.fastForEachIndexed { index, text -> SubPostItem( subPostList = subPosts[index], subPostContent = text, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt index b23bc568..9b9f09d2 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FeedCard.kt @@ -54,10 +54,12 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEachIndexed import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.material.fade import com.google.accompanist.placeholder.material.placeholder import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.huanchengfly.tieba.post.App import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.UserActivity import com.huanchengfly.tieba.post.api.models.protos.Media @@ -85,14 +87,14 @@ import kotlin.math.max import kotlin.math.min private val ImmutableHolder.url: String - @Composable get() = - ImageUtil.getUrl( - LocalContext.current, - true, - get { originPic }, - get { dynamicPic }, - get { bigPic }, - get { srcPic }) + get() = ImageUtil.getUrl( + App.INSTANCE, + true, + get { originPic }, + get { dynamicPic }, + get { bigPic }, + get { srcPic } + ) @Composable private fun DefaultUserHeader( @@ -329,53 +331,65 @@ private fun ThreadMedia( val medias = remember(item) { item.getImmutableList { media } } - val singleMediaFraction = - if (LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Compact) + val hasMedia = remember(medias) { medias.isNotEmpty() } + val isSingleMedia = remember(medias) { medias.size == 1 } + val windowWidthSizeClass = LocalWindowSizeClass.current.widthSizeClass + val singleMediaFraction = remember(windowWidthSizeClass) { + if (windowWidthSizeClass == WindowWidthSizeClass.Compact) 1f else 0.6f - + } if (isVideo) { - val videoInfo = item.getImmutable { videoInfo!! } + val videoInfo = remember(item) { item.getImmutable { videoInfo!! } } + val aspectRatio = remember(videoInfo) { + max( + videoInfo + .get { thumbnailWidth } + .toFloat() / videoInfo.get { thumbnailHeight }, + 16f / 9 + ) + } VideoPlayer( videoUrl = videoInfo.get { videoUrl }, thumbnailUrl = videoInfo.get { thumbnailUrl }, modifier = Modifier .fillMaxWidth(singleMediaFraction) - .aspectRatio( - max( - videoInfo - .get { thumbnailWidth } - .toFloat() / videoInfo.get { thumbnailHeight }, - 16f / 9 - ) - ) + .aspectRatio(aspectRatio) .clip(RoundedCornerShape(8.dp)) ) - } else if (medias.isNotEmpty()) { + } else if (hasMedia) { + val mediaWidthFraction = remember(isSingleMedia, singleMediaFraction) { + if (isSingleMedia) singleMediaFraction else 1f + } + val mediaAspectRatio = remember(isSingleMedia) { + if (isSingleMedia) 2f else 3f + } + val showMediaCount = remember(medias) { min(medias.size, 3) } + val hasMoreMedia = remember(medias) { medias.size > 3 } + val showMedias = remember(medias) { medias.subList(0, showMediaCount) } Box { Row( modifier = Modifier - .fillMaxWidth(if (medias.size == 1) singleMediaFraction else 1f) - .aspectRatio(if (medias.size == 1) 2f else 3f) + .fillMaxWidth(mediaWidthFraction) + .aspectRatio(mediaAspectRatio) .clip(RoundedCornerShape(8.dp)), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - medias.subList(0, min(medias.size, 3)) - .forEachIndexed { index, media -> - val photoViewData = remember(item, index) { - getImmutablePhotoViewData(item.get(), index) - } - NetworkImage( - imageUri = media.url, - contentDescription = null, - modifier = Modifier.weight(1f), - photoViewData = photoViewData, - contentScale = ContentScale.Crop - ) + showMedias.fastForEachIndexed { index, media -> + val photoViewData = remember(item, index) { + getImmutablePhotoViewData(item.get(), index) } + NetworkImage( + imageUri = remember(media) { media.url }, + contentDescription = null, + modifier = Modifier.weight(1f), + photoViewData = photoViewData, + contentScale = ContentScale.Crop + ) + } } - if (medias.size > 3) { + if (hasMoreMedia) { Badge( icon = Icons.Rounded.PhotoSizeSelectActual, text = "${medias.size}", @@ -396,7 +410,8 @@ private fun ThreadForumInfo( val hasForumInfo = remember(item) { item.isNotNull { forumInfo } } if (hasForumInfo) { val forumInfo = remember(item) { item.getImmutable { forumInfo!! } } - if (forumInfo.get { name }.isNotBlank()) { + val hasForum = remember(forumInfo) { forumInfo.get { name }.isNotBlank() } + if (hasForum) { ForumInfoChip( imageUriProvider = { StringUtil.getAvatarUrl(forumInfo.get { avatar }) }, nameProvider = { forumInfo.get { name } }, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt index 36f8c204..436530c7 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt @@ -4,7 +4,9 @@ import android.os.Parcelable import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -38,11 +40,14 @@ fun NetworkImage( } } } else Modifier - AsyncImage( - request = DisplayRequest(context, imageUri) { + val request = remember(imageUri) { + DisplayRequest(context, imageUri) { placeholder(ImageUtil.getPlaceHolder(context, 0)) crossfade() - }, + } + } + AsyncImage( + request = request, contentDescription = contentDescription, modifier = modifier.then(clickableModifier), contentScale = contentScale, @@ -57,29 +62,14 @@ fun NetworkImage( photoViewDataProvider: (() -> ImmutableHolder)? = null, contentScale: ContentScale = ContentScale.Fit, ) { - val imageUri = imageUriProvider() - val photoViewData = photoViewDataProvider?.invoke() + val imageUri by rememberUpdatedState(newValue = imageUriProvider()) + val photoViewData by rememberUpdatedState(newValue = photoViewDataProvider?.invoke()) - val context = LocalContext.current - val clickableModifier = if (photoViewData != null) { - Modifier.clickable( - indication = null, - interactionSource = remember { - MutableInteractionSource() - } - ) { - context.goToActivity { - putExtra(EXTRA_PHOTO_VIEW_DATA, photoViewData.get() as Parcelable) - } - } - } else Modifier - AsyncImage( - request = DisplayRequest(context, imageUri) { - placeholder(ImageUtil.getPlaceHolder(context, 0)) - crossfade() - }, + NetworkImage( + imageUri = imageUri, contentDescription = contentDescription, - modifier = modifier.then(clickableModifier), + modifier = modifier, + photoViewData = photoViewData, contentScale = contentScale, ) } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt index aed0ab02..5c4591ba 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Menu.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable @@ -118,16 +117,15 @@ fun LongClickMenu( indication: Indication? = LocalIndication.current, content: @Composable () -> Unit ) { - val coroutineScope = rememberCoroutineScope() LaunchedEffect(key1 = null) { - coroutineScope.launch { + launch { interactionSource.interactions .filterIsInstance() .collect { menuState.offset = it.pressPosition } } - coroutineScope.launch { + launch { interactionSource.interactions .collect { Log.i("Indication", "$it") @@ -172,18 +170,15 @@ fun LongClickMenu( @Composable fun rememberMenuState(): MenuState { - return rememberSaveable(saver = MenuState.Saver) { - MenuState() - } + return rememberSaveable( + saver = MenuState.Saver, + init = { MenuState() } + ) } @Stable -class MenuState( - expanded: Boolean = false, - offsetX: Float = 0f, - offsetY: Float = 0f, -) { - private var _expanded by mutableStateOf(expanded) +class MenuState { + private var _expanded by mutableStateOf(false) var expanded: Boolean get() = _expanded @@ -193,7 +188,7 @@ class MenuState( } } - private var _offset by mutableStateOf(Offset(offsetX, offsetY)) + private var _offset by mutableStateOf(Offset(0f, 0f)) var offset: Offset get() = _offset @@ -213,11 +208,10 @@ class MenuState( ) }, restore = { - MenuState( - expanded = it[0] as Boolean, - offsetX = it[1] as Float, - offsetY = it[2] as Float, - ) + MenuState().apply { + expanded = it[0] as Boolean + offset = Offset(it[1] as Float, it[2] as Float) + } } ) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/picker/ListPicker.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/picker/ListPicker.kt index 5b2d9206..89d7cc59 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/picker/ListPicker.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/picker/ListPicker.kt @@ -22,14 +22,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.ui.widgets.compose.ProvideContentColor +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf @Composable fun ListSinglePicker( - itemTitles: List, - itemValues: List, + itemTitles: ImmutableList, + itemValues: ImmutableList, selectedPosition: Int, onItemSelected: (position: Int, title: String, value: ItemValue, changed: Boolean) -> Unit, - itemIcons: Map Unit> = emptyMap(), + modifier: Modifier = Modifier, + itemIcons: ImmutableMap Unit> = persistentMapOf(), selectedIndicator: @Composable () -> Unit = { Icon( imageVector = Icons.Rounded.Check, @@ -38,7 +42,6 @@ fun ListSinglePicker( }, colors: PickerColors = PickerDefaults.pickerColors(), enabled: Boolean = true, - modifier: Modifier = Modifier, ) { if (itemTitles.size != itemValues.size) error("titles and values do not match!") Column(modifier = modifier) { @@ -63,7 +66,7 @@ fun ListSinglePicker( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { - (itemIcons.getOrDefault(itemValues[it]) {}).invoke() + itemIcons[itemValues[it]]?.invoke() ProvideContentColor( color = if (selected) colors.selectedItemColor(enabled).value else colors.itemColor( enabled diff --git a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt index 48ed5247..eca03cf2 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/utils/AccountUtil.kt @@ -7,6 +7,7 @@ import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.staticCompositionLocalOf @@ -29,6 +30,7 @@ import org.litepal.extension.findAllAsync import org.litepal.extension.findFirst import java.util.UUID +@Stable object AccountUtil { const val TAG = "AccountUtil" const val ACTION_SWITCH_ACCOUNT = "com.huanchengfly.tieba.post.action.SWITCH_ACCOUNT"