feat: 搜索联想
This commit is contained in:
parent
87ffe0ec6a
commit
7701fceff0
|
|
@ -12,6 +12,7 @@ import com.huanchengfly.tieba.post.api.models.protos.pbFloor.PbFloorResponse
|
|||
import com.huanchengfly.tieba.post.api.models.protos.pbPage.PbPageResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.searchSug.SearchSugResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.threadList.ThreadListResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse
|
||||
|
|
@ -1378,6 +1379,17 @@ interface ITiebaApi {
|
|||
postId: Long,
|
||||
forumId: Long = 0L,
|
||||
page: Int = 1,
|
||||
subPostId: Long = 0L
|
||||
subPostId: Long = 0L,
|
||||
): Flow<PbFloorResponse>
|
||||
|
||||
/**
|
||||
* 搜索联想
|
||||
*
|
||||
* @param keyword 关键词
|
||||
* @param isForum 是否为吧
|
||||
*/
|
||||
fun searchSuggestionsFlow(
|
||||
keyword: String,
|
||||
isForum: Boolean = false,
|
||||
): Flow<SearchSugResponse>
|
||||
}
|
||||
|
|
@ -72,6 +72,9 @@ import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedRe
|
|||
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileRequest
|
||||
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileRequestData
|
||||
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.searchSug.SearchSugRequest
|
||||
import com.huanchengfly.tieba.post.api.models.protos.searchSug.SearchSugRequestData
|
||||
import com.huanchengfly.tieba.post.api.models.protos.searchSug.SearchSugResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.threadList.AdParam
|
||||
import com.huanchengfly.tieba.post.api.models.protos.threadList.ThreadListRequest
|
||||
import com.huanchengfly.tieba.post.api.models.protos.threadList.ThreadListRequestData
|
||||
|
|
@ -1240,4 +1243,20 @@ object MixedTiebaApiImpl : ITiebaApi {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchSuggestionsFlow(keyword: String, isForum: Boolean): Flow<SearchSugResponse> {
|
||||
return RetrofitTiebaApi.OFFICIAL_PROTOBUF_TIEBA_V12_API.searchSugFlow(
|
||||
buildProtobufRequestBody(
|
||||
SearchSugRequest(
|
||||
SearchSugRequestData(
|
||||
common = buildCommonRequest(clientVersion = ClientVersion.TIEBA_V12),
|
||||
word = keyword,
|
||||
isforum = isForum.booleanToString()
|
||||
)
|
||||
),
|
||||
clientVersion = ClientVersion.TIEBA_V12,
|
||||
needSToken = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.huanchengfly.tieba.post.api.models.protos.pbFloor.PbFloorResponse
|
|||
import com.huanchengfly.tieba.post.api.models.protos.pbPage.PbPageResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.personalized.PersonalizedResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.profile.ProfileResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.searchSug.SearchSugResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.threadList.ThreadListResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.topicList.TopicListResponse
|
||||
import com.huanchengfly.tieba.post.api.models.protos.userLike.UserLikeResponse
|
||||
|
|
@ -73,4 +74,9 @@ interface OfficialProtobufTiebaApi {
|
|||
fun addPostFlow(
|
||||
@Body body: MyMultipartBody,
|
||||
): Flow<AddPostResponse>
|
||||
|
||||
@POST("/c/s/searchSug?cmd=309438&format=protobuf")
|
||||
fun searchSugFlow(
|
||||
@Body body: MyMultipartBody,
|
||||
): Flow<SearchSugResponse>
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -57,6 +59,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
|
|
@ -93,6 +96,7 @@ import com.huanchengfly.tieba.post.ui.page.search.user.SearchUserPage
|
|||
import com.huanchengfly.tieba.post.ui.widgets.compose.BaseTextField
|
||||
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.Container
|
||||
import com.huanchengfly.tieba.post.ui.widgets.compose.LazyLoadHorizontalPager
|
||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyBackHandler
|
||||
import com.huanchengfly.tieba.post.ui.widgets.compose.MyScaffold
|
||||
|
|
@ -107,6 +111,8 @@ import kotlinx.collections.immutable.ImmutableMap
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -121,7 +127,7 @@ data class SearchPageItem(
|
|||
val onSelectedSortTypeChange: (Int) -> Unit = {},
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class, FlowPreview::class)
|
||||
@Destination
|
||||
@Composable
|
||||
fun SearchPage(
|
||||
|
|
@ -144,7 +150,25 @@ fun SearchPage(
|
|||
prop1 = SearchUiState::isKeywordEmpty,
|
||||
initial = true
|
||||
)
|
||||
val suggestions by viewModel.uiState.collectPartialAsState(
|
||||
prop1 = SearchUiState::suggestions,
|
||||
initial = persistentListOf()
|
||||
)
|
||||
|
||||
val showSuggestions by remember {
|
||||
derivedStateOf { suggestions.isNotEmpty() }
|
||||
}
|
||||
|
||||
var inputKeyword by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { inputKeyword }
|
||||
.debounce(500)
|
||||
.collect {
|
||||
viewModel.send(SearchUiIntent.KeywordInputChanged(it))
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(keyword) {
|
||||
if (keyword.isNotEmpty() && keyword != inputKeyword) {
|
||||
inputKeyword = keyword
|
||||
|
|
@ -265,21 +289,76 @@ fun SearchPage(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
if (showSuggestions) {
|
||||
SearchSuggestionList(
|
||||
suggestions = suggestions,
|
||||
onItemClick = {
|
||||
inputKeyword = it
|
||||
viewModel.send(SearchUiIntent.SubmitKeyword(it))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Container {
|
||||
SearchHistoryList(
|
||||
searchHistories = searchHistories,
|
||||
onSearchHistoryClick = {
|
||||
inputKeyword = it.content
|
||||
viewModel.send(SearchUiIntent.SubmitKeyword(it.content))
|
||||
},
|
||||
expanded = expanded,
|
||||
onToggleExpand = { expanded = !expanded },
|
||||
onDelete = { viewModel.send(SearchUiIntent.DeleteSearchHistory(it.id)) },
|
||||
onClear = { viewModel.send(SearchUiIntent.ClearSearchHistory) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun SearchSuggestionList(
|
||||
suggestions: ImmutableList<String>,
|
||||
onItemClick: (String) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(
|
||||
items = suggestions,
|
||||
key = { it }
|
||||
) {
|
||||
Container(
|
||||
modifier = Modifier.animateItemPlacement()
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.clickable {
|
||||
onItemClick(it)
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
SearchHistoryList(
|
||||
searchHistories = searchHistories,
|
||||
onSearchHistoryClick = {
|
||||
inputKeyword = it.content
|
||||
viewModel.send(SearchUiIntent.SubmitKeyword(it.content))
|
||||
},
|
||||
expanded = expanded,
|
||||
onToggleExpand = { expanded = !expanded },
|
||||
onDelete = { viewModel.send(SearchUiIntent.DeleteSearchHistory(it.id)) },
|
||||
onClear = { viewModel.send(SearchUiIntent.ClearSearchHistory) }
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = stringResource(id = R.string.desc_search_sug, it),
|
||||
tint = ExtendedTheme.colors.text
|
||||
)
|
||||
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = ExtendedTheme.colors.text
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -287,6 +366,21 @@ fun SearchPage(
|
|||
}
|
||||
}
|
||||
|
||||
@Preview("SearchSuggestionList", backgroundColor = 0xFFFFFFFF)
|
||||
@Composable
|
||||
private fun SearchSuggestionListPreview() {
|
||||
TiebaLiteTheme {
|
||||
Box(
|
||||
modifier = Modifier.background(ExtendedTheme.colors.topBar)
|
||||
) {
|
||||
SearchSuggestionList(
|
||||
suggestions = persistentListOf("1", "2", "3"),
|
||||
onItemClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ColumnScope.SearchTabRow(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.huanchengfly.tieba.post.ui.page.search
|
|||
|
||||
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.models.protos.searchSug.SearchSugResponse
|
||||
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorCode
|
||||
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
|
||||
import com.huanchengfly.tieba.post.arch.BaseViewModel
|
||||
|
|
@ -24,6 +26,7 @@ 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.merge
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.litepal.LitePal
|
||||
|
|
@ -70,6 +73,8 @@ class SearchViewModel :
|
|||
.flatMapConcat { it.producePartialChange() },
|
||||
intentFlow.filterIsInstance<SearchUiIntent.SubmitKeyword>()
|
||||
.flatMapConcat { it.producePartialChange() },
|
||||
intentFlow.filterIsInstance<SearchUiIntent.KeywordInputChanged>()
|
||||
.flatMapConcat { it.producePartialChange() },
|
||||
)
|
||||
|
||||
private fun produceInitPartialChange() = flow<SearchPartialChange.Init> {
|
||||
|
|
@ -109,6 +114,21 @@ class SearchViewModel :
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SearchUiIntent.KeywordInputChanged.producePartialChange() =
|
||||
if (keyword.isNotBlank()) {
|
||||
TiebaApi.getInstance().searchSuggestionsFlow(keyword)
|
||||
.map<SearchSugResponse, SearchPartialChange.KeywordInputChanged> {
|
||||
SearchPartialChange.KeywordInputChanged.Success(
|
||||
it.data_?.list ?: listOf()
|
||||
)
|
||||
}
|
||||
.catch {
|
||||
emit(SearchPartialChange.KeywordInputChanged.Failure(it.getErrorMessage()))
|
||||
}
|
||||
} else {
|
||||
flowOf(SearchPartialChange.KeywordInputChanged.Success(emptyList()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -120,6 +140,8 @@ sealed interface SearchUiIntent : UiIntent {
|
|||
data class DeleteSearchHistory(val id: Long) : SearchUiIntent
|
||||
|
||||
data class SubmitKeyword(val keyword: String) : SearchUiIntent
|
||||
|
||||
data class KeywordInputChanged(val keyword: String) : SearchUiIntent
|
||||
}
|
||||
|
||||
sealed interface SearchPartialChange : PartialChange<SearchUiState> {
|
||||
|
|
@ -167,7 +189,10 @@ sealed interface SearchPartialChange : PartialChange<SearchUiState> {
|
|||
data class SubmitKeyword(val keyword: String) : SearchPartialChange {
|
||||
override fun reduce(oldState: SearchUiState): SearchUiState {
|
||||
if (keyword.isEmpty()) {
|
||||
return oldState.copy(isKeywordEmpty = true)
|
||||
return oldState.copy(
|
||||
isKeywordEmpty = true,
|
||||
suggestions = persistentListOf()
|
||||
)
|
||||
}
|
||||
val newSearchHistories = (oldState.searchHistories
|
||||
.filterNot { it.content == keyword } + SearchHistory(content = keyword))
|
||||
|
|
@ -179,12 +204,26 @@ sealed interface SearchPartialChange : PartialChange<SearchUiState> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class KeywordInputChanged : SearchPartialChange {
|
||||
override fun reduce(oldState: SearchUiState): SearchUiState = when (this) {
|
||||
is Success -> oldState.copy(suggestions = suggestions.toImmutableList())
|
||||
is Failure -> oldState
|
||||
}
|
||||
|
||||
data class Success(val suggestions: List<String>) : KeywordInputChanged()
|
||||
|
||||
data class Failure(
|
||||
val errorMessage: String,
|
||||
) : KeywordInputChanged()
|
||||
}
|
||||
}
|
||||
|
||||
data class SearchUiState(
|
||||
val keyword: String = "",
|
||||
val isKeywordEmpty: Boolean = true,
|
||||
val searchHistories: ImmutableList<SearchHistory> = persistentListOf(),
|
||||
val suggestions: ImmutableList<String> = persistentListOf(),
|
||||
) : UiState
|
||||
|
||||
sealed interface SearchUiEvent : UiEvent {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos";
|
||||
|
||||
message ForumInfo {
|
||||
uint32 forum_id = 1;
|
||||
string forum_name = 2;
|
||||
string avatar = 3;
|
||||
string post_num = 4;
|
||||
string concern_num = 5;
|
||||
int32 has_concerned = 6;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos";
|
||||
|
||||
message RankingParam {
|
||||
int32 rank_type = 1;
|
||||
int32 rank_code = 2;
|
||||
string sort_type = 3;
|
||||
int32 tab_id = 4;
|
||||
int32 pn = 5;
|
||||
int32 rn = 6;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos";
|
||||
|
||||
import "PbContent.proto";
|
||||
|
||||
message RecommendForumInfo {
|
||||
string avatar = 1;
|
||||
uint64 forum_id = 2;
|
||||
string forum_name = 3;
|
||||
uint32 is_like = 4;
|
||||
uint32 member_count = 5;
|
||||
uint32 thread_count = 6;
|
||||
string slogan = 7;
|
||||
repeated PbContent content = 8;
|
||||
uint32 forum_type = 9;
|
||||
string authen = 10;
|
||||
string recom_reason = 11;
|
||||
uint32 is_brand_forum = 12;
|
||||
string hot_text = 13;
|
||||
string abtest_tag = 14;
|
||||
string source = 15;
|
||||
string extra = 16;
|
||||
uint32 is_private_forum = 17;
|
||||
string lv1_name = 18;
|
||||
string lv2_name = 19;
|
||||
string avatar_origin = 20;
|
||||
uint64 hot_thread_id = 22;
|
||||
int32 is_recommend_forum = 23;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba.searchSug;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos.searchSug";
|
||||
|
||||
import "SearchSug/SearchSugRequestData.proto";
|
||||
|
||||
message SearchSugRequest {
|
||||
SearchSugRequestData data = 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba.searchSug;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos.searchSug";
|
||||
|
||||
import "CommonRequest.proto";
|
||||
|
||||
message SearchSugRequestData {
|
||||
CommonRequest common = 1;
|
||||
string word = 2;
|
||||
string isforum = 3;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba.searchSug;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos.searchSug";
|
||||
|
||||
import "Error.proto";
|
||||
import "SearchSug/SearchSugResponseData.proto";
|
||||
|
||||
message SearchSugResponse {
|
||||
Error error = 1;
|
||||
SearchSugResponseData data = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba.searchSug;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos.searchSug";
|
||||
|
||||
import "ForumInfo.proto";
|
||||
import "Item.proto";
|
||||
import "RecommendForumInfo.proto";
|
||||
import "SugLiveInfo.proto";
|
||||
import "SugRankingInfo.proto";
|
||||
|
||||
message SearchSugResponseData {
|
||||
int32 forum_loc = 1;
|
||||
repeated string list = 2;
|
||||
repeated ForumInfo forum_list = 3;
|
||||
RecommendForumInfo forum_card = 4;
|
||||
Item item_card = 5;
|
||||
repeated SugLiveInfo live_card = 6;
|
||||
SugRankingInfo ranking_card = 7;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos";
|
||||
|
||||
import "AlaLiveInfo.proto";
|
||||
|
||||
message SugLiveInfo {
|
||||
string word = 1;
|
||||
AlaLiveInfo ala_info = 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package tieba;
|
||||
|
||||
option java_package = "com.huanchengfly.tieba.post.api.models.protos";
|
||||
|
||||
import "RankingParam.proto";
|
||||
|
||||
message SugRankingInfo {
|
||||
string rank_title = 1;
|
||||
RankingParam rank_param = 2;
|
||||
}
|
||||
|
|
@ -741,4 +741,5 @@
|
|||
<string name="summary_lift_up_bottom_bar">略微抬起贴子页面底栏以方便点按</string>
|
||||
<string name="button_open">打开</string>
|
||||
<string name="btn_hide">隐藏</string>
|
||||
<string name="desc_search_sug">搜索联想:%s</string>
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Reference in New Issue