feat: 首页显示最近浏览的吧

This commit is contained in:
HuanCheng65 2024-02-01 14:09:50 +08:00
parent c7164e2c71
commit 42c640cc8f
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
11 changed files with 255 additions and 51 deletions

View File

@ -9,6 +9,7 @@ import okhttp3.Response
import java.io.IOException import java.io.IOException
import java.net.SocketException import java.net.SocketException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
object ConnectivityInterceptor : Interceptor { object ConnectivityInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
@ -17,7 +18,7 @@ object ConnectivityInterceptor : Interceptor {
val exception = response.exceptionOrNull() val exception = response.exceptionOrNull()
return when { return when {
(exception is SocketTimeoutException || exception is SocketException) && isNetworkConnected() -> throw NoConnectivityException( (exception is SocketTimeoutException || exception is SocketException || exception is SSLHandshakeException) && isNetworkConnected() -> throw NoConnectivityException(
App.INSTANCE.getString(R.string.connectivity_timeout) App.INSTANCE.getString(R.string.connectivity_timeout)
) )

View File

@ -0,0 +1,10 @@
package com.huanchengfly.tieba.post.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ForumHistoryExtra(
@SerialName("forum_id")
val forumId: Long,
)

View File

@ -1,7 +1,9 @@
package com.huanchengfly.tieba.post.models.database package com.huanchengfly.tieba.post.models.database
import androidx.compose.runtime.Immutable
import org.litepal.crud.LitePalSupport import org.litepal.crud.LitePalSupport
@Immutable
data class History( data class History(
val title: String = "", val title: String = "",
val data: String = "", val data: String = "",

View File

@ -21,7 +21,7 @@ import com.huanchengfly.tieba.post.ui.common.theme.utils.ThemeUtils
import com.huanchengfly.tieba.post.utils.AccountUtil import com.huanchengfly.tieba.post.utils.AccountUtil
import com.huanchengfly.tieba.post.utils.ProgressListener import com.huanchengfly.tieba.post.utils.ProgressListener
import com.huanchengfly.tieba.post.utils.SingleAccountSigner import com.huanchengfly.tieba.post.utils.SingleAccountSigner
import com.huanchengfly.tieba.post.utils.addFlag import com.huanchengfly.tieba.post.utils.extension.addFlag
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job

View File

@ -92,6 +92,7 @@ import com.huanchengfly.tieba.post.arch.onEvent
import com.huanchengfly.tieba.post.arch.pageViewModel import com.huanchengfly.tieba.post.arch.pageViewModel
import com.huanchengfly.tieba.post.dataStore import com.huanchengfly.tieba.post.dataStore
import com.huanchengfly.tieba.post.getInt import com.huanchengfly.tieba.post.getInt
import com.huanchengfly.tieba.post.models.ForumHistoryExtra
import com.huanchengfly.tieba.post.models.database.History import com.huanchengfly.tieba.post.models.database.History
import com.huanchengfly.tieba.post.toastShort import com.huanchengfly.tieba.post.toastShort
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@ -133,6 +134,8 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -474,7 +477,8 @@ fun ForumPage(
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
avatar = forum.avatar, avatar = forum.avatar,
type = HistoryUtil.TYPE_FORUM, type = HistoryUtil.TYPE_FORUM,
data = forum.name data = forum.name,
extras = Json.encodeToString(ForumHistoryExtra(forum.id))
), ),
true true
) )

View File

@ -2,11 +2,15 @@ package com.huanchengfly.tieba.post.ui.page.main.home
import android.content.Context import android.content.Context
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -17,19 +21,21 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.DropdownMenuItem import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.outlined.ViewAgenda import androidx.compose.material.icons.outlined.ViewAgenda
import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
@ -37,6 +43,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -47,6 +54,7 @@ import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -83,6 +91,7 @@ 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.LongClickMenu
import com.huanchengfly.tieba.post.ui.widgets.compose.MenuState import com.huanchengfly.tieba.post.ui.widgets.compose.MenuState
import com.huanchengfly.tieba.post.ui.widgets.compose.MyLazyVerticalGrid import com.huanchengfly.tieba.post.ui.widgets.compose.MyLazyVerticalGrid
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
import com.huanchengfly.tieba.post.ui.widgets.compose.TextButton 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.TipScreen
import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar import com.huanchengfly.tieba.post.ui.widgets.compose.Toolbar
@ -394,6 +403,14 @@ fun HomePage(
prop1 = HomeUiState::topForums, prop1 = HomeUiState::topForums,
initial = persistentListOf() initial = persistentListOf()
) )
val historyForums by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::historyForums,
initial = persistentListOf()
)
val showHistoryForum by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::showHistoryForum,
initial = true
)
val error by viewModel.uiState.collectPartialAsState( val error by viewModel.uiState.collectPartialAsState(
prop1 = HomeUiState::error, prop1 = HomeUiState::error,
initial = null initial = null
@ -401,6 +418,7 @@ fun HomePage(
val isLoggedIn = remember(account) { account != null } val isLoggedIn = remember(account) { account != null }
val isEmpty by remember { derivedStateOf { forums.isEmpty() } } val isEmpty by remember { derivedStateOf { forums.isEmpty() } }
val hasTopForum by remember { derivedStateOf { topForums.isNotEmpty() } } val hasTopForum by remember { derivedStateOf { topForums.isNotEmpty() } }
val hasHistoryForum by remember { derivedStateOf { historyForums.isNotEmpty() } }
var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) } var listSingle by remember { mutableStateOf(context.appPreferences.listSingle) }
val isError by remember { derivedStateOf { error != null } } val isError by remember { derivedStateOf { error != null } }
val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } } val gridCells by remember { derivedStateOf { getGridCells(context, listSingle) } }
@ -429,7 +447,11 @@ fun HomePage(
) )
} }
Scaffold( LaunchedEffect(Unit) {
if (viewModel.initialized) viewModel.send(HomeUiIntent.RefreshHistory)
}
MyScaffold(
backgroundColor = Color.Transparent, backgroundColor = Color.Transparent,
topBar = { topBar = {
Toolbar( Toolbar(
@ -464,7 +486,7 @@ fun HomePage(
.padding(contentPaddings) .padding(contentPaddings)
) { ) {
Column { Column {
SearchBox(modifier = Modifier.padding(bottom = 12.dp)) { SearchBox(modifier = Modifier.padding(bottom = 4.dp)) {
navigator.navigate(SearchPageDestination) navigator.navigate(SearchPageDestination)
} }
StateScreen( StateScreen(
@ -496,14 +518,103 @@ fun HomePage(
contentPadding = PaddingValues(bottom = 12.dp), contentPadding = PaddingValues(bottom = 12.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
if (hasHistoryForum) {
item(key = "HistoryForums", span = { GridItemSpan(maxLineSpan) }) {
val rotate by animateFloatAsState(
targetValue = if (showHistoryForum) 90f else 0f,
label = "rotate"
)
Column {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
viewModel.send(
HomeUiIntent.ToggleHistory(
showHistoryForum
)
)
}
.padding(vertical = 8.dp)
.padding(end = 16.dp)
) {
Header(
text = stringResource(id = R.string.title_history_forum),
invert = false
)
Spacer(modifier = Modifier.weight(1f))
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = stringResource(id = R.string.desc_show),
modifier = Modifier
.size(24.dp)
.rotate(rotate)
)
}
AnimatedVisibility(visible = showHistoryForum) {
LazyRow(
contentPadding = PaddingValues(bottom = 8.dp),
) {
item(key = "Spacer1") {
Spacer(modifier = Modifier.width(12.dp))
}
items(
historyForums,
key = { it.data }
) {
Row(
modifier = Modifier
.padding(horizontal = 4.dp)
.height(IntrinsicSize.Min)
.clip(RoundedCornerShape(100))
.background(color = ExtendedTheme.colors.chip)
.clickable {
navigator.navigate(
ForumPageDestination(
it.data
)
)
}
.padding(4.dp),
verticalAlignment = CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Avatar(
data = it.avatar,
contentDescription = null,
size = 24.dp,
shape = CircleShape
)
Text(
text = it.title,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 4.dp)
)
}
}
item(key = "Spacer2") {
Spacer(modifier = Modifier.width(12.dp))
}
}
}
}
}
}
if (hasTopForum) { if (hasTopForum) {
item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) { item(key = "TopForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column { Column(
modifier = Modifier.padding(vertical = 8.dp)
) {
Header( Header(
text = stringResource(id = R.string.title_top_forum), text = stringResource(id = R.string.title_top_forum),
invert = true invert = true
) )
Spacer(modifier = Modifier.height(8.dp))
} }
} }
items( items(
@ -529,11 +640,11 @@ fun HomePage(
isTopForum = true isTopForum = true
) )
} }
}
if (hasHistoryForum || hasTopForum) {
item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) { item(key = "ForumHeader", span = { GridItemSpan(maxLineSpan) }) {
Column( Column(
modifier = Modifier.padding( modifier = Modifier.padding(vertical = 8.dp)
vertical = 8.dp
)
) { ) {
Header(text = stringResource(id = R.string.forum_list_title)) Header(text = stringResource(id = R.string.forum_list_title))
} }
@ -588,7 +699,9 @@ private fun HomePageSkeletonScreen(
.fillMaxSize(), .fillMaxSize(),
) { ) {
item(key = "TopForumHeaderPlaceholder", span = { GridItemSpan(maxLineSpan) }) { item(key = "TopForumHeaderPlaceholder", span = { GridItemSpan(maxLineSpan) }) {
Column { Column(
modifier = Modifier.padding(vertical = 8.dp)
) {
Header( Header(
text = stringResource(id = R.string.title_top_forum), text = stringResource(id = R.string.title_top_forum),
modifier = Modifier.placeholder( modifier = Modifier.placeholder(
@ -597,7 +710,6 @@ private fun HomePageSkeletonScreen(
), ),
invert = true invert = true
) )
Spacer(modifier = Modifier.height(8.dp))
} }
} }
items(6, key = { "TopPlaceholder$it" }) { items(6, key = { "TopPlaceholder$it" }) {

View File

@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.huanchengfly.tieba.post.api.TiebaApi import com.huanchengfly.tieba.post.api.TiebaApi
import com.huanchengfly.tieba.post.api.models.CommonResponse import com.huanchengfly.tieba.post.api.models.CommonResponse
import com.huanchengfly.tieba.post.api.models.protos.forumRecommend.ForumRecommendResponse
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.BaseViewModel import com.huanchengfly.tieba.post.arch.BaseViewModel
import com.huanchengfly.tieba.post.arch.CommonUiEvent import com.huanchengfly.tieba.post.arch.CommonUiEvent
@ -13,8 +12,10 @@ import com.huanchengfly.tieba.post.arch.PartialChangeProducer
import com.huanchengfly.tieba.post.arch.UiEvent import com.huanchengfly.tieba.post.arch.UiEvent
import com.huanchengfly.tieba.post.arch.UiIntent import com.huanchengfly.tieba.post.arch.UiIntent
import com.huanchengfly.tieba.post.arch.UiState import com.huanchengfly.tieba.post.arch.UiState
import com.huanchengfly.tieba.post.models.database.History
import com.huanchengfly.tieba.post.models.database.TopForum import com.huanchengfly.tieba.post.models.database.TopForum
import com.huanchengfly.tieba.post.utils.AccountUtil import com.huanchengfly.tieba.post.utils.AccountUtil
import com.huanchengfly.tieba.post.utils.HistoryUtil
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -25,10 +26,12 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.zip
import org.litepal.LitePal import org.litepal.LitePal
@Stable @Stable
@ -52,18 +55,25 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
return merge( return merge(
intentFlow.filterIsInstance<HomeUiIntent.Refresh>() intentFlow.filterIsInstance<HomeUiIntent.Refresh>()
.flatMapConcat { produceRefreshPartialChangeFlow() }, .flatMapConcat { produceRefreshPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.RefreshHistory>()
.flatMapConcat { produceRefreshHistoryPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>() intentFlow.filterIsInstance<HomeUiIntent.TopForums.Delete>()
.flatMapConcat { it.toPartialChangeFlow() }, .flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.TopForums.Add>() intentFlow.filterIsInstance<HomeUiIntent.TopForums.Add>()
.flatMapConcat { it.toPartialChangeFlow() }, .flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.Unfollow>() intentFlow.filterIsInstance<HomeUiIntent.Unfollow>()
.flatMapConcat { it.toPartialChangeFlow() }, .flatMapConcat { it.toPartialChangeFlow() },
intentFlow.filterIsInstance<HomeUiIntent.ToggleHistory>()
.flatMapConcat { it.toPartialChangeFlow() }
) )
} }
private fun produceRefreshPartialChangeFlow() = @Suppress("USELESS_CAST")
TiebaApi.getInstance().forumRecommendNewFlow() private fun produceRefreshPartialChangeFlow(): Flow<HomePartialChange.Refresh> =
.map<ForumRecommendResponse, HomePartialChange.Refresh> { forumRecommend -> HistoryUtil.getFlow(HistoryUtil.TYPE_FORUM, 0)
.zip(
TiebaApi.getInstance().forumRecommendNewFlow()
) { historyForums, forumRecommend ->
val forums = forumRecommend.data_?.like_forum?.map { val forums = forumRecommend.data_?.like_forum?.map {
HomeUiState.Forum( HomeUiState.Forum(
it.avatar, it.avatar,
@ -76,11 +86,21 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
val topForums = mutableListOf<HomeUiState.Forum>() val topForums = mutableListOf<HomeUiState.Forum>()
val topForumsDB = LitePal.findAll(TopForum::class.java).map { it.forumId } val topForumsDB = LitePal.findAll(TopForum::class.java).map { it.forumId }
topForums.addAll(forums.filter { topForumsDB.contains(it.forumId) }) topForums.addAll(forums.filter { topForumsDB.contains(it.forumId) })
HomePartialChange.Refresh.Success(forums, topForums) HomePartialChange.Refresh.Success(
forums,
topForums,
historyForums
) as HomePartialChange.Refresh
} }
.onStart { emit(HomePartialChange.Refresh.Start) } .onStart { emit(HomePartialChange.Refresh.Start) }
.catch { emit(HomePartialChange.Refresh.Failure(it)) } .catch { emit(HomePartialChange.Refresh.Failure(it)) }
@Suppress("USELESS_CAST")
private fun produceRefreshHistoryPartialChangeFlow(): Flow<HomePartialChange.RefreshHistory> =
HistoryUtil.getFlow(HistoryUtil.TYPE_FORUM, 0)
.map { HomePartialChange.RefreshHistory.Success(it) as HomePartialChange.RefreshHistory }
.catch { emit(HomePartialChange.RefreshHistory.Failure(it)) }
private fun HomeUiIntent.TopForums.Delete.toPartialChangeFlow() = private fun HomeUiIntent.TopForums.Delete.toPartialChangeFlow() =
flow { flow {
val deletedRows = LitePal.deleteAll(TopForum::class.java, "forumId = ?", forumId) val deletedRows = LitePal.deleteAll(TopForum::class.java, "forumId = ?", forumId)
@ -110,11 +130,16 @@ class HomeViewModel : BaseViewModel<HomeUiIntent, HomePartialChange, HomeUiState
HomePartialChange.Unfollow.Success(forumId) HomePartialChange.Unfollow.Success(forumId)
} }
.catch { emit(HomePartialChange.Unfollow.Failure(it.getErrorMessage())) } .catch { emit(HomePartialChange.Unfollow.Failure(it.getErrorMessage())) }
private fun HomeUiIntent.ToggleHistory.toPartialChangeFlow() =
flowOf(HomePartialChange.ToggleHistory(!currentShow))
} }
} }
sealed interface HomeUiIntent : UiIntent { sealed interface HomeUiIntent : UiIntent {
object Refresh : HomeUiIntent data object Refresh : HomeUiIntent
data object RefreshHistory : HomeUiIntent
data class Unfollow(val forumId: String, val forumName: String) : HomeUiIntent data class Unfollow(val forumId: String, val forumName: String) : HomeUiIntent
@ -123,6 +148,8 @@ sealed interface HomeUiIntent : UiIntent {
data class Add(val forum: HomeUiState.Forum) : TopForums data class Add(val forum: HomeUiState.Forum) : TopForums
} }
data class ToggleHistory(val currentShow: Boolean) : HomeUiIntent
} }
sealed interface HomePartialChange : PartialChange<HomeUiState> { sealed interface HomePartialChange : PartialChange<HomeUiState> {
@ -153,6 +180,7 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
isLoading = false, isLoading = false,
forums = forums.toImmutableList(), forums = forums.toImmutableList(),
topForums = topForums.toImmutableList(), topForums = topForums.toImmutableList(),
historyForums = historyForums.toImmutableList(),
error = null error = null
) )
@ -160,18 +188,38 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
Start -> oldState.copy(isLoading = true) Start -> oldState.copy(isLoading = true)
} }
object Start : Refresh() data object Start : Refresh()
data class Success( data class Success(
val forums: List<HomeUiState.Forum>, val forums: List<HomeUiState.Forum>,
val topForums: List<HomeUiState.Forum>, val topForums: List<HomeUiState.Forum>,
val historyForums: List<History>,
) : Refresh() ) : Refresh()
data class Failure( data class Failure(
val error: Throwable val error: Throwable,
) : Refresh() ) : Refresh()
} }
sealed class RefreshHistory : HomePartialChange {
override fun reduce(oldState: HomeUiState): HomeUiState =
when (this) {
is Success -> oldState.copy(
historyForums = historyForums.toImmutableList(),
)
else -> oldState
}
data class Success(
val historyForums: List<History>,
) : RefreshHistory()
data class Failure(
val error: Throwable,
) : RefreshHistory()
}
sealed interface TopForums : HomePartialChange { sealed interface TopForums : HomePartialChange {
sealed interface Delete : HomePartialChange { sealed interface Delete : HomePartialChange {
override fun reduce(oldState: HomeUiState): HomeUiState = override fun reduce(oldState: HomeUiState): HomeUiState =
@ -207,6 +255,11 @@ sealed interface HomePartialChange : PartialChange<HomeUiState> {
data class Failure(val errorMessage: String) : Add data class Failure(val errorMessage: String) : Add
} }
} }
data class ToggleHistory(val show: Boolean) : HomePartialChange {
override fun reduce(oldState: HomeUiState): HomeUiState =
oldState.copy(showHistoryForum = show)
}
} }
@Immutable @Immutable
@ -214,6 +267,8 @@ data class HomeUiState(
val isLoading: Boolean = true, val isLoading: Boolean = true,
val forums: ImmutableList<Forum> = persistentListOf(), val forums: ImmutableList<Forum> = persistentListOf(),
val topForums: ImmutableList<Forum> = persistentListOf(), val topForums: ImmutableList<Forum> = persistentListOf(),
val historyForums: ImmutableList<History> = persistentListOf(),
val showHistoryForum: Boolean = true,
val error: Throwable? = null, val error: Throwable? = null,
) : UiState { ) : UiState {
@Immutable @Immutable

View File

@ -1,15 +1,13 @@
package com.huanchengfly.tieba.post.utils package com.huanchengfly.tieba.post.utils
import com.huanchengfly.tieba.post.models.database.History import com.huanchengfly.tieba.post.models.database.History
import kotlinx.coroutines.Dispatchers import com.huanchengfly.tieba.post.utils.extension.findFlow
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import org.litepal.LitePal
import kotlinx.coroutines.flow.flowOn
import org.litepal.LitePal.deleteAll
import org.litepal.LitePal.order
import org.litepal.LitePal.where
import org.litepal.crud.async.FindMultiExecutor import org.litepal.crud.async.FindMultiExecutor
import org.litepal.extension.deleteAll
import org.litepal.extension.find import org.litepal.extension.find
import org.litepal.extension.findAsync
import org.litepal.extension.findFirstAsync import org.litepal.extension.findFirstAsync
object HistoryUtil { object HistoryUtil {
@ -17,7 +15,7 @@ object HistoryUtil {
const val TYPE_FORUM = 1 const val TYPE_FORUM = 1
const val TYPE_THREAD = 2 const val TYPE_THREAD = 2
fun deleteAll() { fun deleteAll() {
deleteAll(History::class.java) LitePal.deleteAll<History>()
} }
@JvmOverloads @JvmOverloads
@ -30,43 +28,33 @@ object HistoryUtil {
} }
val all: List<History> val all: List<History>
get() = order("timestamp desc, count desc").limit(100).find( get() = LitePal.order("timestamp desc, count desc").limit(100).find<History>()
History::class.java
)
fun getAll(type: Int): List<History> { fun getAll(type: Int): List<History> {
return order("timestamp desc, count desc").where("type = ?", type.toString()) return LitePal.order("timestamp desc, count desc").where("type = ?", type.toString())
.limit(PAGE_SIZE) .limit(PAGE_SIZE)
.find( .find<History>()
History::class.java
)
} }
fun getAllAsync(type: Int): FindMultiExecutor<History> { fun getAllAsync(type: Int): FindMultiExecutor<History> {
return order("timestamp desc, count desc").where("type = ?", type.toString()) return LitePal.order("timestamp desc, count desc").where("type = ?", type.toString())
.limit(PAGE_SIZE) .limit(PAGE_SIZE)
.findAsync( .findAsync<History>()
History::class.java
)
} }
fun getFlow( fun getFlow(
type: Int, type: Int,
page: Int page: Int
): Flow<List<History>> { ): Flow<List<History>> {
return flow<List<History>> { return LitePal.where("type = ?", "$type")
emit( .order("timestamp desc, count desc")
where("type = ?", "$type") .limit(PAGE_SIZE)
.order("timestamp desc, count desc") .offset(page * 100)
.limit(PAGE_SIZE) .findFlow()
.offset(page * 100)
.find()
)
}.flowOn(Dispatchers.IO)
} }
private fun update(history: History): Boolean { private fun update(history: History): Boolean {
val historyBean = where("data = ?", history.data).findFirst( val historyBean = LitePal.where("data = ?", history.data).findFirst(
History::class.java History::class.java
) )
if (historyBean != null) { if (historyBean != null) {
@ -87,7 +75,7 @@ object HistoryUtil {
history: History, history: History,
callback: ((Boolean) -> Unit)? = null, callback: ((Boolean) -> Unit)? = null,
) { ) {
where("data = ?", history.data).findFirstAsync<History?>() LitePal.where("data = ?", history.data).findFirstAsync<History?>()
.listen { .listen {
if (it == null) { if (it == null) {
callback?.invoke(false) callback?.invoke(false)

View File

@ -1,4 +1,4 @@
package com.huanchengfly.tieba.post.utils package com.huanchengfly.tieba.post.utils.extension
/** /**
* 添加flag * 添加flag

View File

@ -0,0 +1,31 @@
package com.huanchengfly.tieba.post.utils.extension
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.litepal.FluentQuery
import org.litepal.LitePal
import org.litepal.extension.find
import org.litepal.extension.findAll
inline fun <reified T> LitePal.findAllFlow(vararg ids: Long): Flow<List<T>> =
flow {
emit(
findAll<T>(*ids)
)
}.flowOn(Dispatchers.IO)
inline fun <reified T> LitePal.findAllFlow(isEager: Boolean, vararg ids: Long): Flow<List<T>> =
flow {
emit(
findAll<T>(isEager, *ids)
)
}.flowOn(Dispatchers.IO)
inline fun <reified T> FluentQuery.findFlow(): Flow<List<T>> =
flow {
emit(
find<T>()
)
}.flowOn(Dispatchers.IO)

View File

@ -514,4 +514,5 @@
<string name="summary_photo_picker_not_supported">当前 Android 版本不支持照片选择器</string> <string name="summary_photo_picker_not_supported">当前 Android 版本不支持照片选择器</string>
<string name="summary_do_not_use_photo_picker">将使用 App 内置的照片选择器</string> <string name="summary_do_not_use_photo_picker">将使用 App 内置的照片选择器</string>
<string name="summary_use_photo_picker">将使用原生的照片选择器</string> <string name="summary_use_photo_picker">将使用原生的照片选择器</string>
<string name="desc_show">显示</string>
</resources> </resources>