feat(SearchPost): 吧内搜索记录
This commit is contained in:
parent
e24649efe9
commit
d86f0d9c41
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<litepal>
|
<litepal>
|
||||||
<dbname value="tblite" />
|
<dbname value="tblite" />
|
||||||
<version value="35" />
|
<version value="36" />
|
||||||
<list>
|
<list>
|
||||||
<mapping class="com.huanchengfly.tieba.post.models.database.Account" />
|
<mapping class="com.huanchengfly.tieba.post.models.database.Account" />
|
||||||
<mapping class="com.huanchengfly.tieba.post.models.database.Draft" />
|
<mapping class="com.huanchengfly.tieba.post.models.database.Draft" />
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,9 @@ class SearchPostActivity : BaseActivity() {
|
||||||
} else {
|
} else {
|
||||||
State.SEARCH
|
State.SEARCH
|
||||||
}
|
}
|
||||||
if (value != null) {
|
if (value != null && forumName != null) {
|
||||||
refreshLayout.autoRefresh()
|
refreshLayout.autoRefresh()
|
||||||
SearchPostHistory(value, forumName)
|
SearchPostHistory(value, forumName!!)
|
||||||
.saveOrUpdate("content = ?", value)
|
.saveOrUpdate("content = ?", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,7 +170,7 @@ class SearchPostActivity : BaseActivity() {
|
||||||
state = State.INPUT
|
state = State.INPUT
|
||||||
editText.showSoftInput()
|
editText.showSoftInput()
|
||||||
} else {
|
} else {
|
||||||
finish()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
package com.huanchengfly.tieba.post.models.database;
|
|
||||||
|
|
||||||
import org.litepal.crud.LitePalSupport;
|
|
||||||
|
|
||||||
public class SearchPostHistory extends LitePalSupport {
|
|
||||||
private int id;
|
|
||||||
private long timestamp;
|
|
||||||
private String content;
|
|
||||||
private String forumName;
|
|
||||||
|
|
||||||
public SearchPostHistory(String content, String forumName) {
|
|
||||||
this.timestamp = System.currentTimeMillis();
|
|
||||||
this.content = content;
|
|
||||||
this.forumName = forumName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(long timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContent() {
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setContent(String content) {
|
|
||||||
this.content = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getForumName() {
|
|
||||||
return forumName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setForumName(String forumName) {
|
|
||||||
this.forumName = forumName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.huanchengfly.tieba.post.models.database
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import org.litepal.crud.LitePalSupport
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class SearchPostHistory(
|
||||||
|
val content: String,
|
||||||
|
val forumName: String,
|
||||||
|
val timestamp: Long = System.currentTimeMillis(),
|
||||||
|
) : LitePalSupport() {
|
||||||
|
val id: Long = 0
|
||||||
|
}
|
||||||
|
|
@ -1,27 +1,38 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.forum.searchpost
|
package com.huanchengfly.tieba.post.ui.page.forum.searchpost
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
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.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.ContentAlpha
|
import androidx.compose.material.ContentAlpha
|
||||||
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.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.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.ArrowDropDown
|
import androidx.compose.material.icons.rounded.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.rounded.ExpandLess
|
||||||
|
import androidx.compose.material.icons.rounded.ExpandMore
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
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
|
||||||
|
|
@ -38,6 +49,7 @@ 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.draw.rotate
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -45,9 +57,11 @@ import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
import com.huanchengfly.tieba.post.R
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
import com.huanchengfly.tieba.post.arch.collectPartialAsState
|
||||||
import com.huanchengfly.tieba.post.arch.pageViewModel
|
import com.huanchengfly.tieba.post.arch.pageViewModel
|
||||||
|
import com.huanchengfly.tieba.post.models.database.SearchPostHistory
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
|
||||||
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
|
||||||
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
|
||||||
|
|
@ -55,6 +69,7 @@ import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.ThreadPageDestination
|
||||||
import com.huanchengfly.tieba.post.ui.page.destinations.UserProfilePageDestination
|
import com.huanchengfly.tieba.post.ui.page.destinations.UserProfilePageDestination
|
||||||
|
import com.huanchengfly.tieba.post.ui.widgets.compose.Button
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu
|
import com.huanchengfly.tieba.post.ui.widgets.compose.ClickMenu
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
|
import com.huanchengfly.tieba.post.ui.widgets.compose.ErrorScreen
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider
|
import com.huanchengfly.tieba.post.ui.widgets.compose.HorizontalDivider
|
||||||
|
|
@ -68,9 +83,121 @@ import com.huanchengfly.tieba.post.ui.widgets.compose.rememberMenuState
|
||||||
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
|
import com.huanchengfly.tieba.post.ui.widgets.compose.states.StateScreen
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
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
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun SearchHistoryList(
|
||||||
|
searchHistories: ImmutableList<SearchPostHistory>,
|
||||||
|
onSearchHistoryClick: (SearchPostHistory) -> Unit,
|
||||||
|
expanded: Boolean = false,
|
||||||
|
onToggleExpand: () -> Unit = {},
|
||||||
|
onDelete: (SearchPostHistory) -> Unit = {},
|
||||||
|
onClear: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val hasItem = remember(searchHistories) {
|
||||||
|
searchHistories.isNotEmpty()
|
||||||
|
}
|
||||||
|
val hasMore = remember(searchHistories) {
|
||||||
|
searchHistories.size > 6
|
||||||
|
}
|
||||||
|
val showItem = remember(expanded, hasMore, searchHistories) {
|
||||||
|
if (!expanded && hasMore) searchHistories.take(6) else searchHistories
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.title_search_history),
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f),
|
||||||
|
style = MaterialTheme.typography.subtitle1
|
||||||
|
)
|
||||||
|
if (hasItem) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.button_clear_all),
|
||||||
|
modifier = Modifier.clickable(onClick = onClear),
|
||||||
|
style = MaterialTheme.typography.button
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.animateContentSize(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
showItem.fastForEach { searchHistory ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(100))
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onSearchHistoryClick(searchHistory) },
|
||||||
|
onLongClick = { onDelete(searchHistory) }
|
||||||
|
)
|
||||||
|
.background(ExtendedTheme.colors.chip)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = searchHistory.content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasMore) {
|
||||||
|
Button(
|
||||||
|
onClick = onToggleExpand,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
backgroundColor = Color.Transparent,
|
||||||
|
contentColor = ExtendedTheme.colors.text
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = if (expanded) R.string.button_expand_less_history else R.string.button_expand_more_history
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.button,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasItem) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.tip_empty),
|
||||||
|
color = ExtendedTheme.colors.textDisabled,
|
||||||
|
fontSize = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
||||||
@Destination
|
@Destination
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -78,7 +205,9 @@ fun ForumSearchPostPage(
|
||||||
forumName: String,
|
forumName: String,
|
||||||
forumId: Long,
|
forumId: Long,
|
||||||
navigator: DestinationsNavigator,
|
navigator: DestinationsNavigator,
|
||||||
viewModel: ForumSearchPostViewModel = pageViewModel(),
|
viewModel: ForumSearchPostViewModel = pageViewModel<ForumSearchPostUiIntent, ForumSearchPostViewModel>(
|
||||||
|
listOf(ForumSearchPostUiIntent.Init)
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val currentKeyword by viewModel.uiState.collectPartialAsState(
|
val currentKeyword by viewModel.uiState.collectPartialAsState(
|
||||||
|
|
@ -117,6 +246,11 @@ fun ForumSearchPostPage(
|
||||||
prop1 = ForumSearchPostUiState::hasMore,
|
prop1 = ForumSearchPostUiState::hasMore,
|
||||||
initial = true
|
initial = true
|
||||||
)
|
)
|
||||||
|
val searchHistories by viewModel.uiState.collectPartialAsState(
|
||||||
|
prop1 = ForumSearchPostUiState::searchHistories,
|
||||||
|
initial = persistentListOf()
|
||||||
|
)
|
||||||
|
|
||||||
val isEmpty by remember {
|
val isEmpty by remember {
|
||||||
derivedStateOf { data.isEmpty() }
|
derivedStateOf { data.isEmpty() }
|
||||||
}
|
}
|
||||||
|
|
@ -453,6 +587,27 @@ fun ForumSearchPostPage(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
SearchHistoryList(
|
||||||
|
searchHistories = searchHistories,
|
||||||
|
onSearchHistoryClick = {
|
||||||
|
viewModel.send(
|
||||||
|
ForumSearchPostUiIntent.Refresh(
|
||||||
|
it.content,
|
||||||
|
forumName,
|
||||||
|
forumId,
|
||||||
|
currentSortType,
|
||||||
|
currentFilterType
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
viewModel.send(ForumSearchPostUiIntent.DeleteHistory(it.id))
|
||||||
|
},
|
||||||
|
onClear = {
|
||||||
|
viewModel.send(ForumSearchPostUiIntent.ClearHistory)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package com.huanchengfly.tieba.post.ui.page.forum.searchpost
|
package com.huanchengfly.tieba.post.ui.page.forum.searchpost
|
||||||
|
|
||||||
|
import com.huanchengfly.tieba.post.App
|
||||||
|
import com.huanchengfly.tieba.post.R
|
||||||
import com.huanchengfly.tieba.post.api.TiebaApi
|
import com.huanchengfly.tieba.post.api.TiebaApi
|
||||||
import com.huanchengfly.tieba.post.api.models.SearchThreadBean
|
import com.huanchengfly.tieba.post.api.models.SearchThreadBean
|
||||||
|
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.ImmutableHolder
|
import com.huanchengfly.tieba.post.arch.ImmutableHolder
|
||||||
import com.huanchengfly.tieba.post.arch.PartialChange
|
import com.huanchengfly.tieba.post.arch.PartialChange
|
||||||
import com.huanchengfly.tieba.post.arch.PartialChangeProducer
|
import com.huanchengfly.tieba.post.arch.PartialChangeProducer
|
||||||
|
|
@ -10,18 +14,29 @@ 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.arch.wrapImmutable
|
import com.huanchengfly.tieba.post.arch.wrapImmutable
|
||||||
|
import com.huanchengfly.tieba.post.models.database.SearchPostHistory
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
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
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
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.flowOf
|
||||||
|
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.onEach
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import org.litepal.LitePal
|
||||||
|
import org.litepal.extension.delete
|
||||||
|
import org.litepal.extension.deleteAll
|
||||||
|
import org.litepal.extension.find
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -32,20 +47,69 @@ class ForumSearchPostViewModel @Inject constructor() :
|
||||||
override fun createPartialChangeProducer(): PartialChangeProducer<ForumSearchPostUiIntent, ForumSearchPostPartialChange, ForumSearchPostUiState> =
|
override fun createPartialChangeProducer(): PartialChangeProducer<ForumSearchPostUiIntent, ForumSearchPostPartialChange, ForumSearchPostUiState> =
|
||||||
ForumSearchPostPartialChangeProducer
|
ForumSearchPostPartialChangeProducer
|
||||||
|
|
||||||
|
override fun dispatchEvent(partialChange: ForumSearchPostPartialChange): UiEvent? =
|
||||||
|
when (partialChange) {
|
||||||
|
is ForumSearchPostPartialChange.DeleteHistory.Failure -> CommonUiEvent.Toast(
|
||||||
|
App.INSTANCE.getString(
|
||||||
|
R.string.toast_delete_failure,
|
||||||
|
partialChange.error.getErrorMessage()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is ForumSearchPostPartialChange.ClearHistory.Success -> CommonUiEvent.Toast(
|
||||||
|
App.INSTANCE.getString(R.string.toast_clear_success)
|
||||||
|
)
|
||||||
|
|
||||||
|
is ForumSearchPostPartialChange.ClearHistory.Failure -> CommonUiEvent.Toast(
|
||||||
|
App.INSTANCE.getString(
|
||||||
|
R.string.toast_clear_failure,
|
||||||
|
partialChange.error.getErrorMessage()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
private object ForumSearchPostPartialChangeProducer :
|
private object ForumSearchPostPartialChangeProducer :
|
||||||
PartialChangeProducer<ForumSearchPostUiIntent, ForumSearchPostPartialChange, ForumSearchPostUiState> {
|
PartialChangeProducer<ForumSearchPostUiIntent, ForumSearchPostPartialChange, ForumSearchPostUiState> {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override fun toPartialChangeFlow(intentFlow: Flow<ForumSearchPostUiIntent>): Flow<ForumSearchPostPartialChange> =
|
override fun toPartialChangeFlow(intentFlow: Flow<ForumSearchPostUiIntent>): Flow<ForumSearchPostPartialChange> =
|
||||||
merge(
|
merge(
|
||||||
|
intentFlow.filterIsInstance<ForumSearchPostUiIntent.Init>()
|
||||||
|
.flatMapConcat { produceInitPartialChange() },
|
||||||
intentFlow.filterIsInstance<ForumSearchPostUiIntent.Refresh>()
|
intentFlow.filterIsInstance<ForumSearchPostUiIntent.Refresh>()
|
||||||
.flatMapConcat { it.producePartialChange() },
|
.flatMapConcat { it.producePartialChange() },
|
||||||
intentFlow.filterIsInstance<ForumSearchPostUiIntent.LoadMore>()
|
intentFlow.filterIsInstance<ForumSearchPostUiIntent.LoadMore>()
|
||||||
.flatMapConcat { it.producePartialChange() },
|
.flatMapConcat { it.producePartialChange() },
|
||||||
|
intentFlow.filterIsInstance<ForumSearchPostUiIntent.DeleteHistory>()
|
||||||
|
.flatMapConcat { it.producePartialChange() },
|
||||||
|
intentFlow.filterIsInstance<ForumSearchPostUiIntent.ClearHistory>()
|
||||||
|
.flatMapConcat { produceClearHistoryPartialChange() },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun produceInitPartialChange(): Flow<ForumSearchPostPartialChange.Init> =
|
||||||
|
flow<ForumSearchPostPartialChange.Init> {
|
||||||
|
val searchHistories = LitePal
|
||||||
|
.order("timestamp DESC")
|
||||||
|
.find<SearchPostHistory>()
|
||||||
|
emit(ForumSearchPostPartialChange.Init.Success(searchHistories))
|
||||||
|
}.catch {
|
||||||
|
emit(ForumSearchPostPartialChange.Init.Failure(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private fun ForumSearchPostUiIntent.Refresh.producePartialChange(): Flow<ForumSearchPostPartialChange.Refresh> =
|
private fun ForumSearchPostUiIntent.Refresh.producePartialChange(): Flow<ForumSearchPostPartialChange.Refresh> =
|
||||||
TiebaApi.getInstance()
|
flowOf(keyword.trim())
|
||||||
.searchPostFlow(keyword, forumName, forumId, sortType, filterType)
|
.filter { it.isNotBlank() }
|
||||||
|
.onEach {
|
||||||
|
runCatching {
|
||||||
|
SearchPostHistory(it, forumName).saveOrUpdate("content = ?", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flatMapConcat {
|
||||||
|
TiebaApi.getInstance()
|
||||||
|
.searchPostFlow(it, forumName, forumId, sortType, filterType)
|
||||||
|
}
|
||||||
.map<SearchThreadBean, ForumSearchPostPartialChange.Refresh> {
|
.map<SearchThreadBean, ForumSearchPostPartialChange.Refresh> {
|
||||||
val postList = it.data.postList.toImmutableList()
|
val postList = it.data.postList.toImmutableList()
|
||||||
ForumSearchPostPartialChange.Refresh.Success(
|
ForumSearchPostPartialChange.Refresh.Success(
|
||||||
|
|
@ -60,12 +124,14 @@ class ForumSearchPostViewModel @Inject constructor() :
|
||||||
emit(
|
emit(
|
||||||
ForumSearchPostPartialChange.Refresh.Start(
|
ForumSearchPostPartialChange.Refresh.Start(
|
||||||
keyword,
|
keyword,
|
||||||
|
forumName,
|
||||||
sortType,
|
sortType,
|
||||||
filterType
|
filterType
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.catch { emit(ForumSearchPostPartialChange.Refresh.Failure(it)) }
|
.catch { emit(ForumSearchPostPartialChange.Refresh.Failure(it)) }
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
private fun ForumSearchPostUiIntent.LoadMore.producePartialChange(): Flow<ForumSearchPostPartialChange.LoadMore> =
|
private fun ForumSearchPostUiIntent.LoadMore.producePartialChange(): Flow<ForumSearchPostPartialChange.LoadMore> =
|
||||||
TiebaApi.getInstance()
|
TiebaApi.getInstance()
|
||||||
|
|
@ -83,10 +149,28 @@ class ForumSearchPostViewModel @Inject constructor() :
|
||||||
}
|
}
|
||||||
.onStart { emit(ForumSearchPostPartialChange.LoadMore.Start) }
|
.onStart { emit(ForumSearchPostPartialChange.LoadMore.Start) }
|
||||||
.catch { emit(ForumSearchPostPartialChange.LoadMore.Failure(it)) }
|
.catch { emit(ForumSearchPostPartialChange.LoadMore.Failure(it)) }
|
||||||
|
|
||||||
|
private fun ForumSearchPostUiIntent.DeleteHistory.producePartialChange(): Flow<ForumSearchPostPartialChange.DeleteHistory> =
|
||||||
|
flow<ForumSearchPostPartialChange.DeleteHistory> {
|
||||||
|
LitePal.delete<SearchPostHistory>(id)
|
||||||
|
emit(ForumSearchPostPartialChange.DeleteHistory.Success(id))
|
||||||
|
}.catch {
|
||||||
|
emit(ForumSearchPostPartialChange.DeleteHistory.Failure(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun produceClearHistoryPartialChange(): Flow<ForumSearchPostPartialChange.ClearHistory> =
|
||||||
|
flow<ForumSearchPostPartialChange.ClearHistory> {
|
||||||
|
LitePal.deleteAll<SearchPostHistory>()
|
||||||
|
emit(ForumSearchPostPartialChange.ClearHistory.Success)
|
||||||
|
}.catch {
|
||||||
|
emit(ForumSearchPostPartialChange.ClearHistory.Failure(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ForumSearchPostUiIntent : UiIntent {
|
sealed interface ForumSearchPostUiIntent : UiIntent {
|
||||||
|
data object Init : ForumSearchPostUiIntent
|
||||||
|
|
||||||
data class Refresh(
|
data class Refresh(
|
||||||
val keyword: String,
|
val keyword: String,
|
||||||
val forumName: String,
|
val forumName: String,
|
||||||
|
|
@ -103,20 +187,52 @@ sealed interface ForumSearchPostUiIntent : UiIntent {
|
||||||
val sortType: Int,
|
val sortType: Int,
|
||||||
val filterType: Int,
|
val filterType: Int,
|
||||||
) : ForumSearchPostUiIntent
|
) : ForumSearchPostUiIntent
|
||||||
|
|
||||||
|
data class DeleteHistory(val id: Long) : ForumSearchPostUiIntent
|
||||||
|
|
||||||
|
data object ClearHistory : ForumSearchPostUiIntent
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ForumSearchPostPartialChange : PartialChange<ForumSearchPostUiState> {
|
sealed interface ForumSearchPostPartialChange : PartialChange<ForumSearchPostUiState> {
|
||||||
|
sealed class Init : ForumSearchPostPartialChange {
|
||||||
|
override fun reduce(oldState: ForumSearchPostUiState): ForumSearchPostUiState =
|
||||||
|
when (this) {
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
searchHistories = searchHistories.toImmutableList()
|
||||||
|
)
|
||||||
|
|
||||||
|
is Failure -> oldState
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Success(
|
||||||
|
val searchHistories: List<SearchPostHistory>,
|
||||||
|
) : Init()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable,
|
||||||
|
) : Init()
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Refresh : ForumSearchPostPartialChange {
|
sealed class Refresh : ForumSearchPostPartialChange {
|
||||||
override fun reduce(oldState: ForumSearchPostUiState): ForumSearchPostUiState =
|
override fun reduce(oldState: ForumSearchPostUiState): ForumSearchPostUiState =
|
||||||
when (this) {
|
when (this) {
|
||||||
is Start -> oldState.copy(
|
is Start -> {
|
||||||
isRefreshing = true,
|
val newSearchHistories = (oldState.searchHistories
|
||||||
isLoadingMore = false,
|
.filterNot { it.content == keyword } + SearchPostHistory(
|
||||||
error = null,
|
keyword,
|
||||||
keyword = keyword,
|
forumName
|
||||||
sortType = sortType,
|
))
|
||||||
filterType = filterType,
|
.sortedByDescending { it.timestamp }
|
||||||
)
|
oldState.copy(
|
||||||
|
isRefreshing = true,
|
||||||
|
isLoadingMore = false,
|
||||||
|
error = null,
|
||||||
|
searchHistories = newSearchHistories.toImmutableList(),
|
||||||
|
keyword = keyword,
|
||||||
|
sortType = sortType,
|
||||||
|
filterType = filterType,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is Success -> oldState.copy(
|
is Success -> oldState.copy(
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
|
|
@ -139,6 +255,7 @@ sealed interface ForumSearchPostPartialChange : PartialChange<ForumSearchPostUiS
|
||||||
|
|
||||||
data class Start(
|
data class Start(
|
||||||
val keyword: String,
|
val keyword: String,
|
||||||
|
val forumName: String,
|
||||||
val sortType: Int,
|
val sortType: Int,
|
||||||
val filterType: Int,
|
val filterType: Int,
|
||||||
) : Refresh()
|
) : Refresh()
|
||||||
|
|
@ -196,12 +313,48 @@ sealed interface ForumSearchPostPartialChange : PartialChange<ForumSearchPostUiS
|
||||||
val error: Throwable,
|
val error: Throwable,
|
||||||
) : LoadMore()
|
) : LoadMore()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class DeleteHistory : ForumSearchPostPartialChange {
|
||||||
|
override fun reduce(oldState: ForumSearchPostUiState): ForumSearchPostUiState =
|
||||||
|
when (this) {
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
searchHistories = oldState.searchHistories.filterNot { it.id == id }
|
||||||
|
.toImmutableList()
|
||||||
|
)
|
||||||
|
|
||||||
|
is Failure -> oldState
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Success(val id: Long) : DeleteHistory()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable,
|
||||||
|
) : DeleteHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ClearHistory : ForumSearchPostPartialChange {
|
||||||
|
override fun reduce(oldState: ForumSearchPostUiState): ForumSearchPostUiState =
|
||||||
|
when (this) {
|
||||||
|
is Success -> oldState.copy(
|
||||||
|
searchHistories = persistentListOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
is Failure -> oldState
|
||||||
|
}
|
||||||
|
|
||||||
|
data object Success : ClearHistory()
|
||||||
|
|
||||||
|
data class Failure(
|
||||||
|
val error: Throwable,
|
||||||
|
) : ClearHistory()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ForumSearchPostUiState(
|
data class ForumSearchPostUiState(
|
||||||
val isRefreshing: Boolean = true,
|
val isRefreshing: Boolean = true,
|
||||||
val isLoadingMore: Boolean = false,
|
val isLoadingMore: Boolean = false,
|
||||||
val error: ImmutableHolder<Throwable>? = null,
|
val error: ImmutableHolder<Throwable>? = null,
|
||||||
|
val searchHistories: ImmutableList<SearchPostHistory> = persistentListOf(),
|
||||||
val currentPage: Int = 1,
|
val currentPage: Int = 1,
|
||||||
val hasMore: Boolean = true,
|
val hasMore: Boolean = true,
|
||||||
val keyword: String = "",
|
val keyword: String = "",
|
||||||
|
|
|
||||||
|
|
@ -104,16 +104,15 @@ class SearchViewModel :
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
private fun SearchUiIntent.SubmitKeyword.producePartialChange() =
|
private fun SearchUiIntent.SubmitKeyword.producePartialChange() =
|
||||||
flowOf(SearchPartialChange.SubmitKeyword(keyword.trim()))
|
flowOf(keyword.trim())
|
||||||
.onEach {
|
.onEach {
|
||||||
runCatching {
|
if (it.isNotBlank()) {
|
||||||
val trimKeyword = keyword.trim()
|
runCatching {
|
||||||
if (trimKeyword.isNotBlank()) SearchHistory(trimKeyword).saveOrUpdate(
|
SearchHistory(it).saveOrUpdate("content = ?", it)
|
||||||
"content = ?",
|
}
|
||||||
trimKeyword
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.map { SearchPartialChange.SubmitKeyword(it) }
|
||||||
|
|
||||||
private fun SearchUiIntent.KeywordInputChanged.producePartialChange() =
|
private fun SearchUiIntent.KeywordInputChanged.producePartialChange() =
|
||||||
if (keyword.isNotBlank()) {
|
if (keyword.isNotBlank()) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue