feat(SearchPost): 吧内搜索记录

This commit is contained in:
HuanCheng65 2024-01-29 17:14:12 +08:00
parent e24649efe9
commit d86f0d9c41
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
7 changed files with 342 additions and 66 deletions

View File

@ -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" />

View File

@ -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()
} }
} }

View File

@ -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;
}
}

View File

@ -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
}

View File

@ -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)
}
)
} }
} }
} }

View File

@ -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 = "",

View File

@ -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()) {