feat: 新版收藏页面
This commit is contained in:
parent
0a834c81ab
commit
257dd0ee38
|
|
@ -158,7 +158,6 @@
|
|||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:host="forum"
|
||||
android:scheme="tblite" />
|
||||
</intent-filter>
|
||||
|
||||
|
|
|
|||
|
|
@ -499,6 +499,19 @@ interface ITiebaApi {
|
|||
pageSize: Int = 20
|
||||
): Call<ThreadStoreBean>
|
||||
|
||||
/**
|
||||
* 查看收藏贴列表
|
||||
*
|
||||
* **需登录**
|
||||
*
|
||||
* @param page 分页页码(从 0 开始)
|
||||
* @param pageSize 每页贴数(默认 20)
|
||||
*/
|
||||
fun threadStoreFlow(
|
||||
page: Int = 0,
|
||||
pageSize: Int = 20
|
||||
): Flow<ThreadStoreBean>
|
||||
|
||||
/**
|
||||
* 移除收藏
|
||||
*
|
||||
|
|
@ -512,6 +525,17 @@ interface ITiebaApi {
|
|||
tbs: String
|
||||
): Call<CommonResponse>
|
||||
|
||||
/**
|
||||
* 移除收藏
|
||||
*
|
||||
* **需登录**
|
||||
*
|
||||
* @param threadId 贴子 ID
|
||||
*/
|
||||
fun removeStoreFlow(
|
||||
threadId: String
|
||||
): Flow<CommonResponse>
|
||||
|
||||
/**
|
||||
* 添加/更新收藏
|
||||
*
|
||||
|
|
|
|||
|
|
@ -375,9 +375,18 @@ object MixedTiebaApiImpl : ITiebaApi {
|
|||
AccountUtil.getUid()
|
||||
)
|
||||
|
||||
override fun threadStoreFlow(page: Int, pageSize: Int): Flow<ThreadStoreBean> =
|
||||
RetrofitTiebaApi.OFFICIAL_TIEBA_API.threadStoreFlow(
|
||||
pageSize,
|
||||
pageSize * page
|
||||
)
|
||||
|
||||
override fun removeStore(threadId: String, tbs: String): Call<CommonResponse> =
|
||||
RetrofitTiebaApi.NEW_TIEBA_API.removeStore(threadId, tbs)
|
||||
|
||||
override fun removeStoreFlow(threadId: String): Flow<CommonResponse> =
|
||||
RetrofitTiebaApi.OFFICIAL_TIEBA_API.removeStoreFlow(threadId)
|
||||
|
||||
override fun addStore(threadId: String, postId: String, tbs: String): Call<CommonResponse> =
|
||||
RetrofitTiebaApi.NEW_TIEBA_API.addStore(
|
||||
listOf(
|
||||
|
|
|
|||
|
|
@ -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<CommonResponse>
|
||||
|
||||
@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<ThreadStoreBean>
|
||||
|
||||
@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<CommonResponse>
|
||||
}
|
||||
|
|
@ -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<ThreadStoreBean?>, t: Throwable) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!!
|
||||
)
|
||||
)
|
||||
},
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UserCollectActivity>()
|
||||
navigator.navigate(ThreadStorePageDestination)
|
||||
}
|
||||
)
|
||||
ListMenuItem(
|
||||
|
|
|
|||
|
|
@ -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<ThreadStoreUiEvent.Delete.Failure>(viewModel) {
|
||||
scaffoldState.snackbarHostState.showSnackbar(
|
||||
context.getString(
|
||||
R.string.delete_store_failure,
|
||||
it.errorMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
onEvent<ThreadStoreUiEvent.Delete.Success>(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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ThreadStoreUiIntent, ThreadStorePartialChange, ThreadStoreUiState, ThreadStoreUiEvent>() {
|
||||
override fun createInitialState(): ThreadStoreUiState = ThreadStoreUiState()
|
||||
|
||||
override fun createPartialChangeProducer(): PartialChangeProducer<ThreadStoreUiIntent, ThreadStorePartialChange, ThreadStoreUiState> =
|
||||
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<ThreadStoreUiIntent, ThreadStorePartialChange, ThreadStoreUiState> {
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun toPartialChangeFlow(intentFlow: Flow<ThreadStoreUiIntent>): Flow<ThreadStorePartialChange> =
|
||||
merge(
|
||||
intentFlow.filterIsInstance<ThreadStoreUiIntent.Refresh>()
|
||||
.flatMapConcat { produceRefreshPartialChange() },
|
||||
intentFlow.filterIsInstance<ThreadStoreUiIntent.LoadMore>()
|
||||
.flatMapConcat { it.producePartialChange() },
|
||||
intentFlow.filterIsInstance<ThreadStoreUiIntent.Delete>()
|
||||
.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<CommonResponse, ThreadStorePartialChange.Delete> {
|
||||
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<ThreadStoreUiState> {
|
||||
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<ThreadStoreBean.ThreadStoreInfo>,
|
||||
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<ThreadStoreBean.ThreadStoreInfo>,
|
||||
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<ThreadStoreBean.ThreadStoreInfo> = emptyList()
|
||||
) : UiState
|
||||
|
||||
sealed interface ThreadStoreUiEvent : UiEvent {
|
||||
sealed interface Delete : ThreadStoreUiEvent {
|
||||
object Success : Delete
|
||||
|
||||
data class Failure(
|
||||
val errorCode: Int,
|
||||
val errorMsg: String
|
||||
) : Delete
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,4 +624,6 @@
|
|||
<string name="launcher_not_support_pin_shortcut">启动器不支持创建快捷方式</string>
|
||||
<string name="load_shortcut_icon_fail">加载图标失败</string>
|
||||
<string name="sort_menu">排序菜单</string>
|
||||
<string name="delete_store_success">取消收藏成功</string>
|
||||
<string name="delete_store_failure">取消收藏失败 %s</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue