feat: 首页显示最近浏览的吧
This commit is contained in:
parent
c7164e2c71
commit
42c640cc8f
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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 = "",
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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" }) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.huanchengfly.tieba.post.utils
|
package com.huanchengfly.tieba.post.utils.extension
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加flag
|
* 添加flag
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue