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