feat: 新增搜索历史与AI智能体搜索功能
- 新增搜索历史记录功能,使用 SharedPreferences + JSON 进行本地存储。 - 搜索页在无搜索结果时展示历史记录,支持点击搜索、长按删除单个记录和清空全部历史。 - 新增 "AI" 搜索标签页,用于根据关键字搜索智能体(Agent)。 - 搜索页离开时自动重置搜索状态和文本,返回后显示历史记录。 - 优化了搜索逻辑,在输入文本为空时自动隐藏搜索结果并显示历史记录。
This commit is contained in:
@@ -108,6 +108,15 @@ interface AgentService {
|
|||||||
authorId: Int? = null
|
authorId: Int? = null
|
||||||
): ListContainer<AgentEntity>?
|
): ListContainer<AgentEntity>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据标题关键字搜索智能体
|
||||||
|
*/
|
||||||
|
suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int = 20,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,35 @@ class AgentPagingSource(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能体搜索分页加载器(按标题关键字)
|
||||||
|
*/
|
||||||
|
class AgentSearchPagingSource(
|
||||||
|
private val agentRemoteDataSource: AgentRemoteDataSource,
|
||||||
|
private val keyword: String,
|
||||||
|
) : PagingSource<Int, AgentEntity>() {
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, AgentEntity> {
|
||||||
|
return try {
|
||||||
|
val currentPage = params.key ?: 1
|
||||||
|
val agents = agentRemoteDataSource.searchAgentByTitle(
|
||||||
|
pageNumber = currentPage,
|
||||||
|
title = keyword
|
||||||
|
)
|
||||||
|
LoadResult.Page(
|
||||||
|
data = agents?.list ?: listOf(),
|
||||||
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
|
nextKey = if (agents?.list?.isNotEmpty() == true) currentPage + 1 else null
|
||||||
|
)
|
||||||
|
} catch (exception: IOException) {
|
||||||
|
LoadResult.Error(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, AgentEntity>): Int? {
|
||||||
|
return state.anchorPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class AgentRemoteDataSource(
|
class AgentRemoteDataSource(
|
||||||
private val agentService: AgentService,
|
private val agentService: AgentService,
|
||||||
@@ -103,6 +132,16 @@ class AgentRemoteDataSource(
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentService.searchAgentByTitle(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentServiceImpl() : AgentService {
|
class AgentServiceImpl() : AgentService {
|
||||||
@@ -118,6 +157,17 @@ class AgentServiceImpl() : AgentService {
|
|||||||
authorId = authorId
|
authorId = authorId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
pageSize: Int,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
return agentBackend.searchAgentByTitle(
|
||||||
|
pageNumber = pageNumber,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AgentBackend {
|
class AgentBackend {
|
||||||
@@ -175,6 +225,27 @@ class AgentBackend {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun searchAgentByTitle(
|
||||||
|
pageNumber: Int,
|
||||||
|
title: String
|
||||||
|
): ListContainer<AgentEntity>? {
|
||||||
|
val resp = ApiClient.api.getAgent(
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
withWorkflow = 1,
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
val body = resp.body() ?: return null
|
||||||
|
val dataContainer = body as DataContainer<ListContainer<Agent>>
|
||||||
|
val listContainer = dataContainer.data
|
||||||
|
return ListContainer(
|
||||||
|
total = listContainer.total,
|
||||||
|
page = pageNumber,
|
||||||
|
pageSize = DataBatchSize,
|
||||||
|
list = listContainer.list.map { it.toAgentEntity() }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AgentEntity(
|
data class AgentEntity(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.search
|
package com.aiosman.ravenow.ui.index.tabs.search
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -10,6 +11,8 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -32,14 +35,13 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.material.Button
|
import androidx.compose.material.Button
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Tab
|
|
||||||
import androidx.compose.material.TabRow
|
|
||||||
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.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@@ -85,10 +87,8 @@ fun SearchScreen() {
|
|||||||
val AppColors = LocalAppTheme.current
|
val AppColors = LocalAppTheme.current
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val model = SearchViewModel
|
val model = SearchViewModel
|
||||||
val categories = listOf(context.getString(R.string.moment), context.getString(R.string.users))
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(pageCount = { categories.size })
|
val pagerState = rememberPagerState(pageCount = { 3 })
|
||||||
val selectedTabIndex = remember { derivedStateOf { pagerState.currentPage } }
|
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val systemUiController = rememberSystemUiController()
|
val systemUiController = rememberSystemUiController()
|
||||||
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
val statusBarPaddingValues = WindowInsets.systemBars.asPaddingValues()
|
||||||
@@ -100,11 +100,19 @@ fun SearchScreen() {
|
|||||||
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
systemUiController.setStatusBarColor(Color.Transparent, darkIcons = !AppState.darkMode)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
model.ensureInit(context)
|
||||||
if (model.requestFocus) {
|
if (model.requestFocus) {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
model.requestFocus = false
|
model.requestFocus = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
// 离开页面时重置搜索状态与文本
|
||||||
|
model.searchText = ""
|
||||||
|
model.ResetModel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -130,7 +138,7 @@ fun SearchScreen() {
|
|||||||
.weight(1f),
|
.weight(1f),
|
||||||
text = model.searchText,
|
text = model.searchText,
|
||||||
onTextChange = {
|
onTextChange = {
|
||||||
model.searchText = it
|
model.onTextChanged(it)
|
||||||
},
|
},
|
||||||
onSearch = {
|
onSearch = {
|
||||||
model.search()
|
model.search()
|
||||||
@@ -144,13 +152,33 @@ fun SearchScreen() {
|
|||||||
stringResource(R.string.cancel),
|
stringResource(R.string.cancel),
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
modifier = Modifier.noRippleClickable {
|
modifier = Modifier.noRippleClickable {
|
||||||
|
// 退出时也重置,确保返回后显示历史而不是上次结果
|
||||||
|
model.searchText = ""
|
||||||
|
model.ResetModel()
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
},
|
},
|
||||||
color = AppColors.text
|
color = AppColors.text
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 历史搜索(当不展示结果时)
|
||||||
|
if (!model.showResult) {
|
||||||
|
HistorySection(
|
||||||
|
onClear = { model.clearHistory() },
|
||||||
|
onClick = { term ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
keyboardController?.hide()
|
||||||
|
model.onTextChanged(term)
|
||||||
|
pagerState.scrollToPage(0)
|
||||||
|
model.search()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRemove = { term -> model.removeHistoryItem(term) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 添加user、dynamic和ai标签页
|
// 添加user、dynamic和ai标签页
|
||||||
|
if (model.showResult) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -188,31 +216,17 @@ fun SearchScreen() {
|
|||||||
text = stringResource(R.string.chat_ai),
|
text = stringResource(R.string.chat_ai),
|
||||||
isSelected = pagerState.currentPage == 2,
|
isSelected = pagerState.currentPage == 2,
|
||||||
onClick = {
|
onClick = {
|
||||||
// TODO: 实现点击逻辑
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (model.showResult) {
|
if (model.showResult) {
|
||||||
TabRow(
|
|
||||||
selectedTabIndex = selectedTabIndex.value,
|
|
||||||
backgroundColor = AppColors.background,
|
|
||||||
contentColor = AppColors.text,
|
|
||||||
) {
|
|
||||||
categories.forEachIndexed { index, category ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedTabIndex.value == index,
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch {
|
|
||||||
pagerState.animateScrollToPage(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = { Text(category, color = AppColors.text) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SearchPager(
|
SearchPager(
|
||||||
pagerState = pagerState
|
pagerState = pagerState
|
||||||
)
|
)
|
||||||
@@ -220,6 +234,67 @@ fun SearchScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HistorySection(
|
||||||
|
onClear: () -> Unit,
|
||||||
|
onClick: (String) -> Unit,
|
||||||
|
onRemove: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val model = SearchViewModel
|
||||||
|
val items = model.historyFlow.collectAsState(initial = emptyList()).value
|
||||||
|
if (items.isEmpty()) return
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "历史搜索",
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
text = "清空",
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier = Modifier.noRippleClickable { onClear() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items.forEach { term ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(AppColors.inputBackground, RoundedCornerShape(16.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick(term) },
|
||||||
|
onLongClick = { onRemove(term) }
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = term,
|
||||||
|
color = AppColors.text,
|
||||||
|
fontSize = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchInput(
|
fun SearchInput(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -300,6 +375,7 @@ fun SearchPager(
|
|||||||
when (page) {
|
when (page) {
|
||||||
0 -> MomentResultTab()
|
0 -> MomentResultTab()
|
||||||
1 -> UserResultTab()
|
1 -> UserResultTab()
|
||||||
|
2 -> AiResultTab()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -566,6 +642,118 @@ fun UserItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Composable
|
@Composable
|
||||||
|
fun AiResultTab() {
|
||||||
|
val AppColors = LocalAppTheme.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val model = SearchViewModel
|
||||||
|
val agents = model.agentsFlow.collectAsLazyPagingItems()
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(AppColors.background)
|
||||||
|
) {
|
||||||
|
if (agents.itemCount == 0 && model.showResult) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
val isNetworkAvailable = NetworkUtils.isNetworkAvailable(context)
|
||||||
|
if (isNetworkAvailable) {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(
|
||||||
|
id = if (AppState.darkMode) R.mipmap.syss_yh_qs_as_img
|
||||||
|
else R.mipmap.invalid_name_1
|
||||||
|
),
|
||||||
|
contentDescription = "No Result",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "咦,什么都没找到...",
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "换个关键词试试吧,也许会有新发现!",
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = painterResource(id = R.mipmap.invalid_name_10),
|
||||||
|
contentDescription = "network error",
|
||||||
|
modifier = Modifier.size(140.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_title),
|
||||||
|
color = LocalAppTheme.current.text,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.W600
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.friend_chat_no_network_subtitle),
|
||||||
|
color = LocalAppTheme.current.secondaryText,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.W400
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
ReloadButton(
|
||||||
|
onClick = {
|
||||||
|
SearchViewModel.ResetModel()
|
||||||
|
SearchViewModel.search()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(agents.itemCount) { idx ->
|
||||||
|
val agent = agents[idx] ?: return@items
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
CustomAsyncImage(
|
||||||
|
context,
|
||||||
|
imageUrl = agent.avatar,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(RoundedCornerShape(24.dp)),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = agent.title,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = AppColors.text
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = agent.desc,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
color = AppColors.secondaryText,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
fun ReloadButton(
|
fun ReloadButton(
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.aiosman.ravenow.ui.index.tabs.search
|
package com.aiosman.ravenow.ui.index.tabs.search
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -18,6 +19,11 @@ import com.aiosman.ravenow.entity.MomentEntity
|
|||||||
import com.aiosman.ravenow.entity.MomentPagingSource
|
import com.aiosman.ravenow.entity.MomentPagingSource
|
||||||
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
import com.aiosman.ravenow.entity.MomentRemoteDataSource
|
||||||
import com.aiosman.ravenow.entity.MomentServiceImpl
|
import com.aiosman.ravenow.entity.MomentServiceImpl
|
||||||
|
import com.aiosman.ravenow.entity.AgentEntity
|
||||||
|
import com.aiosman.ravenow.entity.AgentRemoteDataSource
|
||||||
|
import com.aiosman.ravenow.entity.AgentSearchPagingSource
|
||||||
|
import com.aiosman.ravenow.entity.AgentServiceImpl
|
||||||
|
import com.aiosman.ravenow.utils.SearchHistoryStore
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -32,12 +38,37 @@ object SearchViewModel : ViewModel() {
|
|||||||
private val userService = UserServiceImpl()
|
private val userService = UserServiceImpl()
|
||||||
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
private val _usersFlow = MutableStateFlow<PagingData<AccountProfileEntity>>(PagingData.empty())
|
||||||
val usersFlow = _usersFlow.asStateFlow()
|
val usersFlow = _usersFlow.asStateFlow()
|
||||||
|
private val _agentsFlow = MutableStateFlow<PagingData<AgentEntity>>(PagingData.empty())
|
||||||
|
val agentsFlow = _agentsFlow.asStateFlow()
|
||||||
|
private lateinit var historyStore: SearchHistoryStore
|
||||||
|
private val _historyFlow = MutableStateFlow<List<String>>(emptyList())
|
||||||
|
val historyFlow = _historyFlow.asStateFlow()
|
||||||
var showResult by mutableStateOf(false)
|
var showResult by mutableStateOf(false)
|
||||||
var requestFocus by mutableStateOf(false)
|
var requestFocus by mutableStateOf(false)
|
||||||
|
|
||||||
|
fun ensureInit(context: Context) {
|
||||||
|
if (!::historyStore.isInitialized) {
|
||||||
|
historyStore = SearchHistoryStore(context.applicationContext)
|
||||||
|
_historyFlow.value = historyStore.getHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTextChanged(newText: String) {
|
||||||
|
searchText = newText
|
||||||
|
if (newText.isBlank()) {
|
||||||
|
showResult = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun search() {
|
fun search() {
|
||||||
if (searchText.isEmpty()) {
|
if (searchText.isEmpty()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 记录历史
|
||||||
|
if (::historyStore.isInitialized) {
|
||||||
|
historyStore.add(searchText)
|
||||||
|
_historyFlow.value = historyStore.getHistory()
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Pager(
|
Pager(
|
||||||
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
@@ -64,9 +95,34 @@ object SearchViewModel : ViewModel() {
|
|||||||
_usersFlow.value = it
|
_usersFlow.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
Pager(
|
||||||
|
config = PagingConfig(pageSize = 5, enablePlaceholders = false),
|
||||||
|
pagingSourceFactory = {
|
||||||
|
AgentSearchPagingSource(
|
||||||
|
AgentRemoteDataSource(AgentServiceImpl()),
|
||||||
|
keyword = searchText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).flow.cachedIn(viewModelScope).collectLatest {
|
||||||
|
_agentsFlow.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
showResult = true
|
showResult = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun removeHistoryItem(term: String) {
|
||||||
|
if (!::historyStore.isInitialized) return
|
||||||
|
historyStore.remove(term)
|
||||||
|
_historyFlow.value = historyStore.getHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearHistory() {
|
||||||
|
if (!::historyStore.isInitialized) return
|
||||||
|
historyStore.clear()
|
||||||
|
_historyFlow.value = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun followUser(id:Int){
|
suspend fun followUser(id:Int){
|
||||||
userService.followUser(id.toString())
|
userService.followUser(id.toString())
|
||||||
val currentPagingData = _usersFlow.value
|
val currentPagingData = _usersFlow.value
|
||||||
@@ -96,6 +152,7 @@ object SearchViewModel : ViewModel() {
|
|||||||
fun ResetModel(){
|
fun ResetModel(){
|
||||||
_momentsFlow.value = PagingData.empty()
|
_momentsFlow.value = PagingData.empty()
|
||||||
_usersFlow.value = PagingData.empty()
|
_usersFlow.value = PagingData.empty()
|
||||||
|
_agentsFlow.value = PagingData.empty()
|
||||||
showResult = false
|
showResult = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.aiosman.ravenow.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索历史存储(SharedPreferences + JSON)
|
||||||
|
* - 最多保留 maxSize 条
|
||||||
|
* - 新记录放首位,去重(忽略前后空格)
|
||||||
|
*/
|
||||||
|
class SearchHistoryStore(context: Context) {
|
||||||
|
private val prefs: SharedPreferences =
|
||||||
|
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
fun getHistory(): List<String> {
|
||||||
|
val json = prefs.getString(KEY_HISTORY, "[]") ?: "[]"
|
||||||
|
return runCatching {
|
||||||
|
val type = object : TypeToken<List<String>>() {}.type
|
||||||
|
gson.fromJson<List<String>>(json, type) ?: emptyList()
|
||||||
|
}.getOrDefault(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun add(term: String) {
|
||||||
|
val normalized = term.trim()
|
||||||
|
if (normalized.isEmpty()) return
|
||||||
|
val current = getHistory().toMutableList()
|
||||||
|
current.removeAll { it.equals(normalized, ignoreCase = true) }
|
||||||
|
current.add(0, normalized)
|
||||||
|
while (current.size > MAX_SIZE) current.removeLast()
|
||||||
|
save(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(term: String) {
|
||||||
|
val normalized = term.trim()
|
||||||
|
val current = getHistory().toMutableList()
|
||||||
|
current.removeAll { it.equals(normalized, ignoreCase = true) }
|
||||||
|
save(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
save(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(list: List<String>) {
|
||||||
|
prefs.edit().putString(KEY_HISTORY, gson.toJson(list)).apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREF_NAME = "search_history_pref"
|
||||||
|
private const val KEY_HISTORY = "search_history_v1"
|
||||||
|
private const val MAX_SIZE = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user