diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 535d8545..05366d8e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -158,7 +158,6 @@ diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt index c902f1c8..1133b27a 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/ITiebaApi.kt @@ -499,6 +499,19 @@ interface ITiebaApi { pageSize: Int = 20 ): Call + /** + * 查看收藏贴列表 + * + * **需登录** + * + * @param page 分页页码(从 0 开始) + * @param pageSize 每页贴数(默认 20) + */ + fun threadStoreFlow( + page: Int = 0, + pageSize: Int = 20 + ): Flow + /** * 移除收藏 * @@ -512,6 +525,17 @@ interface ITiebaApi { tbs: String ): Call + /** + * 移除收藏 + * + * **需登录** + * + * @param threadId 贴子 ID + */ + fun removeStoreFlow( + threadId: String + ): Flow + /** * 添加/更新收藏 * diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt index 09a81730..8bbd65f4 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/interfaces/impls/MixedTiebaApiImpl.kt @@ -375,9 +375,18 @@ object MixedTiebaApiImpl : ITiebaApi { AccountUtil.getUid() ) + override fun threadStoreFlow(page: Int, pageSize: Int): Flow = + RetrofitTiebaApi.OFFICIAL_TIEBA_API.threadStoreFlow( + pageSize, + pageSize * page + ) + override fun removeStore(threadId: String, tbs: String): Call = RetrofitTiebaApi.NEW_TIEBA_API.removeStore(threadId, tbs) + override fun removeStoreFlow(threadId: String): Flow = + RetrofitTiebaApi.OFFICIAL_TIEBA_API.removeStoreFlow(threadId) + override fun addStore(threadId: String, postId: String, tbs: String): Call = RetrofitTiebaApi.NEW_TIEBA_API.addStore( listOf( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt index 76c1588a..143334cb 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/api/retrofit/interfaces/OfficialTiebaApi.kt @@ -311,4 +311,41 @@ interface OfficialTiebaApi { @retrofit2.http.Header(Header.USER_AGENT) user_agent: String = "bdtb for Android $client_version", @Field("stoken") stoken: String? = AccountUtil.getSToken(), ): Flow + + @POST("/c/f/post/threadstore") + @FormUrlEncoded + @Headers( + "${Header.FORCE_LOGIN}: ${Header.FORCE_LOGIN_TRUE}", + "${Header.COOKIE}: ka=open", + "${Header.DROP_HEADERS}: ${Header.CHARSET},${Header.CLIENT_TYPE}", + "${Header.NO_COMMON_PARAMS}: ${Param.OAID}", + ) + fun threadStoreFlow( + @Field("rn") pageSize: Int, + @Field("offset") offset: Int, + @retrofit2.http.Header("client_user_token") client_user_token: String? = AccountUtil.getUid(), + @Field("_client_version") client_version: String = "11.10.8.6", + @retrofit2.http.Header(Header.USER_AGENT) user_agent: String = "bdtb for Android $client_version", + @Field("stoken") stoken: String? = AccountUtil.getSToken(), + @Field("user_id") user_id: String? = AccountUtil.getUid(), + ): Flow + + @POST("/c/c/post/rmstore") + @FormUrlEncoded + @Headers( + "${Header.FORCE_LOGIN}: ${Header.FORCE_LOGIN_TRUE}", + "${Header.COOKIE}: ka=open", + "${Header.DROP_HEADERS}: ${Header.CHARSET},${Header.CLIENT_TYPE}", + "${Header.NO_COMMON_PARAMS}: ${Param.OAID}", + ) + fun removeStoreFlow( + @Field("tid") threadId: String, + @Field("tbs") tbs: String = AccountUtil.getLoginInfo()!!.tbs, + @Field("stoken") stoken: String = AccountUtil.getSToken()!!, + @Field("user_id") user_id: String? = AccountUtil.getUid(), + @Field("fid") fid: String = "null", + @Field("_client_version") client_version: String = "11.10.8.6", + @retrofit2.http.Header(Header.USER_AGENT) user_agent: String = "bdtb for Android $client_version", + @retrofit2.http.Header("client_user_token") client_user_token: String? = user_id, + ): Flow } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/fragments/ThreadStoreFragment.kt b/app/src/main/java/com/huanchengfly/tieba/post/fragments/ThreadStoreFragment.kt index a9ce6273..69b100a1 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/fragments/ThreadStoreFragment.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/fragments/ThreadStoreFragment.kt @@ -184,7 +184,7 @@ class ThreadStoreFragment : BaseFragment() { val storeInfoList = data!!.storeThread ?: return threadStoreAdapter.reset() threadStoreAdapter.setData(storeInfoList) - hasMore = storeInfoList.size > 0 + hasMore = storeInfoList.isNotEmpty() } override fun onFailure(call: Call, t: Throwable) { 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 514124be..1d9a3ffa 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 @@ -144,7 +144,13 @@ private fun Header( invert: Boolean = false, modifier: Modifier = Modifier ) { - Chip(text = text, modifier = modifier.padding(start = 16.dp), invertColor = invert) + Chip( + text = text, + modifier = Modifier + .padding(start = 16.dp) + .then(modifier), + invertColor = invert + ) } @Composable diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/notifications/list/NotificationsListPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/notifications/list/NotificationsListPage.kt index ac1db019..7f705f4c 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/notifications/list/NotificationsListPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/notifications/list/NotificationsListPage.kt @@ -120,10 +120,25 @@ fun NotificationsListPage( contentDescription = null ) }, - name = { Text(text = it.replyer.nameShow ?: it.replyer.name ?: "") }, - desc = { Text(text = DateTimeUtils.getRelativeTimeString(LocalContext.current, it.time!!)) }, + name = { + Text( + text = it.replyer.nameShow ?: it.replyer.name ?: "" + ) + }, onClick = { - UserActivity.launch(context, it.replyer.id!!, StringUtil.getAvatarUrl(it.replyer.portrait)) + UserActivity.launch( + context, + it.replyer.id!!, + StringUtil.getAvatarUrl(it.replyer.portrait) + ) + }, + desc = { + Text( + text = DateTimeUtils.getRelativeTimeString( + LocalContext.current, + it.time!! + ) + ) }, ) {} } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/user/UserPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/user/UserPage.kt index aee73229..dc83dffb 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/user/UserPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/main/user/UserPage.kt @@ -49,7 +49,6 @@ import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.activities.AppThemeActivity import com.huanchengfly.tieba.post.activities.HistoryActivity import com.huanchengfly.tieba.post.activities.UserActivity -import com.huanchengfly.tieba.post.activities.UserCollectActivity import com.huanchengfly.tieba.post.activities.WebViewActivity import com.huanchengfly.tieba.post.arch.collectPartialAsState import com.huanchengfly.tieba.post.arch.pageViewModel @@ -59,6 +58,7 @@ 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.destinations.AboutPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.SettingsPageDestination +import com.huanchengfly.tieba.post.ui.page.destinations.ThreadStorePageDestination import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes @@ -312,7 +312,7 @@ fun UserPage( icon = ImageVector.vectorResource(id = R.drawable.ic_favorite), text = stringResource(id = R.string.title_my_collect), onClick = { - context.goToActivity() + navigator.navigate(ThreadStorePageDestination) } ) ListMenuItem( diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStorePage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStorePage.kt index f391bc16..2603ec54 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStorePage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStorePage.kt @@ -1,2 +1,280 @@ package com.huanchengfly.tieba.post.ui.page.threadstore +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material.DropdownMenuItem +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.activities.ThreadActivity +import com.huanchengfly.tieba.post.activities.UserActivity +import com.huanchengfly.tieba.post.api.models.ThreadStoreBean +import com.huanchengfly.tieba.post.arch.collectPartialAsState +import com.huanchengfly.tieba.post.arch.onEvent +import com.huanchengfly.tieba.post.arch.pageViewModel +import com.huanchengfly.tieba.post.dpToPxFloat +import com.huanchengfly.tieba.post.pxToSp +import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.ui.widgets.compose.Avatar +import com.huanchengfly.tieba.post.ui.widgets.compose.BackNavigationIcon +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.LongClickMenu +import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold +import com.huanchengfly.tieba.post.ui.widgets.compose.Sizes +import com.huanchengfly.tieba.post.ui.widgets.compose.TitleCentredToolbar +import com.huanchengfly.tieba.post.ui.widgets.compose.UserHeader +import com.huanchengfly.tieba.post.utils.StringUtil +import com.huanchengfly.tieba.post.utils.StringUtil.getUsernameAnnotatedString +import com.huanchengfly.tieba.post.utils.appPreferences +import com.ramcosta.composedestinations.annotation.DeepLink +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator + +private val UpdateTipTextStyle = TextStyle(fontWeight = FontWeight.Bold, fontSize = 10.sp) + +@OptIn(ExperimentalMaterialApi::class, ExperimentalTextApi::class) +@Destination( + deepLinks = [ + DeepLink(uriPattern = "tblite://favorite") + ] +) +@Composable +fun ThreadStorePage( + navigator: DestinationsNavigator, + viewModel: ThreadStoreViewModel = pageViewModel() +) { + LazyLoad(loaded = viewModel.initialized) { + viewModel.send(ThreadStoreUiIntent.Refresh) + viewModel.initialized = true + } + val isRefreshing by viewModel.uiState.collectPartialAsState( + prop1 = ThreadStoreUiState::isRefreshing, + initial = false + ) + val isLoadingMore by viewModel.uiState.collectPartialAsState( + prop1 = ThreadStoreUiState::isLoadingMore, + initial = false + ) + val hasMore by viewModel.uiState.collectPartialAsState( + prop1 = ThreadStoreUiState::hasMore, + initial = true + ) + val currentPage by viewModel.uiState.collectPartialAsState( + prop1 = ThreadStoreUiState::currentPage, + initial = 0 + ) + val data by viewModel.uiState.collectPartialAsState( + prop1 = ThreadStoreUiState::data, + initial = emptyList() + ) + + val context = LocalContext.current + val scaffoldState = rememberScaffoldState() + val pullRefreshState = rememberPullRefreshState( + refreshing = isRefreshing, + onRefresh = { viewModel.send(ThreadStoreUiIntent.Refresh) }) + LaunchedEffect(null) { + onEvent(viewModel) { + scaffoldState.snackbarHostState.showSnackbar( + context.getString( + R.string.delete_store_failure, + it.errorMsg + ) + ) + } + onEvent(viewModel) { + scaffoldState.snackbarHostState.showSnackbar(context.getString(R.string.delete_store_success)) + } + } + MyScaffold( + scaffoldState = scaffoldState, + topBar = { + TitleCentredToolbar( + title = stringResource(id = R.string.title_my_collect), + navigationIcon = { + BackNavigationIcon(onBackPressed = { navigator.navigateUp() }) + } + ) + } + ) { paddingValues -> + val textMeasurer = rememberTextMeasurer() + + Box(modifier = Modifier.pullRefresh(pullRefreshState)) { + LoadMoreLayout( + isLoading = isLoadingMore, + onLoadMore = { viewModel.send(ThreadStoreUiIntent.LoadMore(currentPage + 1)) }, + loadEnd = !hasMore + ) { + LazyColumn(contentPadding = paddingValues) { + items( + items = data, + key = { it.threadId } + ) { info -> + LongClickMenu( + menuContent = { + DropdownMenuItem(onClick = { + viewModel.send( + ThreadStoreUiIntent.Delete( + info.threadId + ) + ) + }) { + Text(text = stringResource(id = R.string.title_collect_on)) + } + }, + onClick = { + ThreadActivity.launch( + context, + info.threadId, + info.markPid, + context.appPreferences.collectThreadSeeLz, + "collect", + info.maxPid + ) + } + ) { + StoreItem( + info = info, + onUserClick = { + info.author.lzUid?.let { + UserActivity.launch( + context, + it, StringUtil.getAvatarUrl(info.author.userPortrait) + ) + } + }, + textMeasurer = textMeasurer + ) + } + } + } + } + PullRefreshIndicator( + refreshing = isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } +} + +@OptIn(ExperimentalTextApi::class) +@Composable +private fun StoreItem( + info: ThreadStoreBean.ThreadStoreInfo, + onUserClick: () -> Unit, + modifier: Modifier = Modifier, + textMeasurer: TextMeasurer = rememberTextMeasurer() +) { + val hasUpdate = info.count != "0" && info.postNo != "0" + var width = 0 + var height = 0 + Column( + modifier = modifier + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + UserHeader( + avatar = { + Avatar( + data = StringUtil.getAvatarUrl(info.author.userPortrait), + size = Sizes.Small, + contentDescription = null + ) + }, + name = { + Text( + text = getUsernameAnnotatedString( + info.author.name ?: "", + info.author.nameShow + ) + ) + }, + onClick = onUserClick, + ) + val title = buildAnnotatedString { + append(info.title) + if (hasUpdate) { + append(" ") + appendInlineContent("Update", info.postNo) + } + } + val updateTip = stringResource( + id = R.string.tip_thread_store_update, + info.postNo + ) + if (hasUpdate) { + val result = textMeasurer.measure( + AnnotatedString(updateTip), + style = UpdateTipTextStyle + ) + width = + result.size.width.pxToSp() + 12F.dpToPxFloat().pxToSp() * 2 + 1 + height = result.size.height.pxToSp() + 4F.dpToPxFloat().pxToSp() * 2 + } + Text( + text = title, + fontSize = 15.sp, + color = ExtendedTheme.colors.text, + inlineContent = mapOf( + "Update" to InlineTextContent( + placeholder = Placeholder( + width = width.sp, + height = height.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter + ), + children = { + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = ExtendedTheme.colors.chip, + shape = RoundedCornerShape(3.dp) + ) + .padding(vertical = 4.dp, horizontal = 12.dp) + ) { + Text( + text = updateTip, + style = UpdateTipTextStyle, + color = ExtendedTheme.colors.onChip, + modifier = Modifier.align(Alignment.Center) + ) + } + } + ) + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStoreViewModel.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStoreViewModel.kt new file mode 100644 index 00000000..5c6f3e8f --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/threadstore/ThreadStoreViewModel.kt @@ -0,0 +1,186 @@ +package com.huanchengfly.tieba.post.ui.page.threadstore + +import com.huanchengfly.tieba.post.api.TiebaApi +import com.huanchengfly.tieba.post.api.models.CommonResponse +import com.huanchengfly.tieba.post.api.models.ThreadStoreBean +import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode +import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage +import com.huanchengfly.tieba.post.arch.BaseViewModel +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 dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import javax.inject.Inject + +@HiltViewModel +class ThreadStoreViewModel @Inject constructor() : + BaseViewModel() { + override fun createInitialState(): ThreadStoreUiState = ThreadStoreUiState() + + override fun createPartialChangeProducer(): PartialChangeProducer = + ThreadStorePartialChangeProducer + + override fun dispatchEvent(partialChange: ThreadStorePartialChange): UiEvent? { + return when (partialChange) { + is ThreadStorePartialChange.Delete.Success -> ThreadStoreUiEvent.Delete.Success + is ThreadStorePartialChange.Delete.Failure -> ThreadStoreUiEvent.Delete.Failure( + partialChange.error.getErrorCode(), + partialChange.error.getErrorMessage() + ) + + else -> null + } + } + + private object ThreadStorePartialChangeProducer : + PartialChangeProducer { + @OptIn(FlowPreview::class) + override fun toPartialChangeFlow(intentFlow: Flow): Flow = + merge( + intentFlow.filterIsInstance() + .flatMapConcat { produceRefreshPartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, + intentFlow.filterIsInstance() + .flatMapConcat { it.producePartialChange() }, + ) + + private fun produceRefreshPartialChange() = + TiebaApi.getInstance() + .threadStoreFlow() + .map { + if (it.storeThread != null) ThreadStorePartialChange.Refresh.Success( + it.storeThread, + it.storeThread.isNotEmpty() + ) + else ThreadStorePartialChange.Refresh.Failure(NullPointerException("未知错误")) + } + .onStart { emit(ThreadStorePartialChange.Refresh.Start) } + .catch { emit(ThreadStorePartialChange.Refresh.Failure(it)) } + + private fun ThreadStoreUiIntent.LoadMore.producePartialChange() = + TiebaApi.getInstance() + .threadStoreFlow(page) + .map { + if (it.storeThread != null) ThreadStorePartialChange.LoadMore.Success( + it.storeThread, + it.storeThread.isNotEmpty(), + page + ) + else ThreadStorePartialChange.LoadMore.Failure(NullPointerException("未知错误")) + } + .onStart { emit(ThreadStorePartialChange.LoadMore.Start) } + .catch { emit(ThreadStorePartialChange.LoadMore.Failure(it)) } + + private fun ThreadStoreUiIntent.Delete.producePartialChange() = + TiebaApi.getInstance() + .removeStoreFlow(threadId) + .map { + ThreadStorePartialChange.Delete.Success(threadId) + } + .catch { emit(ThreadStorePartialChange.Delete.Failure(it)) } + } +} + +sealed interface ThreadStoreUiIntent : UiIntent { + object Refresh : ThreadStoreUiIntent + + data class LoadMore(val page: Int) : ThreadStoreUiIntent + + data class Delete(val threadId: String) : ThreadStoreUiIntent +} + +sealed interface ThreadStorePartialChange : PartialChange { + sealed class Refresh : ThreadStorePartialChange { + override fun reduce(oldState: ThreadStoreUiState): ThreadStoreUiState = when (this) { + is Failure -> oldState.copy(isRefreshing = false) + Start -> oldState.copy(isRefreshing = true) + is Success -> oldState.copy( + isRefreshing = false, + data = data, + currentPage = 0, + hasMore = hasMore + ) + } + + object Start : Refresh() + + data class Success( + val data: List, + val hasMore: Boolean + ) : Refresh() + + data class Failure( + val error: Throwable + ) : Refresh() + } + + sealed class LoadMore : ThreadStorePartialChange { + override fun reduce(oldState: ThreadStoreUiState): ThreadStoreUiState = when (this) { + is Failure -> oldState.copy(isLoadingMore = false) + Start -> oldState.copy(isLoadingMore = true) + is Success -> oldState.copy( + isLoadingMore = false, + data = oldState.data + data, + currentPage = currentPage, + hasMore = hasMore + ) + } + + object Start : LoadMore() + + data class Success( + val data: List, + val hasMore: Boolean, + val currentPage: Int + ) : LoadMore() + + data class Failure( + val error: Throwable + ) : LoadMore() + } + + sealed class Delete : ThreadStorePartialChange { + override fun reduce(oldState: ThreadStoreUiState): ThreadStoreUiState = when (this) { + is Failure -> oldState + is Success -> oldState.copy(data = oldState.data.filterNot { it.threadId == threadId }) + } + + data class Success( + val threadId: String + ) : Delete() + + data class Failure( + val error: Throwable + ) : Delete() + } +} + +data class ThreadStoreUiState( + val isRefreshing: Boolean = false, + val isLoadingMore: Boolean = false, + val hasMore: Boolean = true, + val currentPage: Int = 0, + val data: List = emptyList() +) : UiState + +sealed interface ThreadStoreUiEvent : UiEvent { + sealed interface Delete : ThreadStoreUiEvent { + object Success : Delete + + data class Failure( + val errorCode: Int, + val errorMsg: String + ) : Delete + } +} \ No newline at end of file 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 7fcf05a1..b5100159 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 @@ -96,6 +96,13 @@ private fun DefaultUserHeader( color = ExtendedTheme.colors.text ) }, + onClick = { + UserActivity.launch( + context, + user.id.toString(), + StringUtil.getAvatarUrl(user.portrait) + ) + }, desc = { Text( text = DateTimeUtils.getRelativeTimeString( @@ -104,13 +111,6 @@ private fun DefaultUserHeader( ) ) }, - onClick = { - UserActivity.launch( - context, - user.id.toString(), - StringUtil.getAvatarUrl(user.portrait) - ) - }, content = content ) } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Headers.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Headers.kt index 5446ab6e..b95b373e 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Headers.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Headers.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle @@ -26,9 +25,9 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme fun UserHeader( avatar: @Composable () -> Unit, name: @Composable () -> Unit, - desc: @Composable () -> Unit, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, + desc: @Composable (() -> Unit)? = null, content: @Composable (RowScope.() -> Unit)? = null, ) { val clickableModifier = if (onClick != null) { @@ -49,7 +48,10 @@ fun UserHeader( avatar() } Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { Box(modifier = clickableModifier) { ProvideTextStyle( value = MaterialTheme.typography.subtitle1.merge( @@ -62,17 +64,18 @@ fun UserHeader( name() } } - Spacer(modifier = Modifier.height(2.dp)) - Box(modifier = clickableModifier) { - ProvideTextStyle( - value = MaterialTheme.typography.caption.merge( - TextStyle( - color = ExtendedTheme.colors.textSecondary, - fontSize = 11.sp + if (desc != null) { + Box(modifier = clickableModifier) { + ProvideTextStyle( + value = MaterialTheme.typography.caption.merge( + TextStyle( + color = ExtendedTheme.colors.textSecondary, + fontSize = 11.sp + ) ) - ) - ) { - desc() + ) { + desc() + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 272bb388..10339c7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -624,4 +624,6 @@ 启动器不支持创建快捷方式 加载图标失败 排序菜单 + 取消收藏成功 + 取消收藏失败 %s