Add article search feature

This commit is contained in:
Ash 2022-04-08 22:44:48 +08:00
parent aaf032332b
commit 25009e9036
8 changed files with 535 additions and 134 deletions

View File

@ -10,6 +10,198 @@ import java.util.*
@Dao @Dao
interface ArticleDao { interface ArticleDao {
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId IN (
SELECT id FROM feed WHERE groupId = :groupId
)
AND isUnread = :isUnread
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByGroupIdWhenIsUnread(
accountId: Int,
text: String,
groupId: String,
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId IN (
SELECT id FROM feed WHERE groupId = :groupId
)
AND isStarred = :isStarred
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByGroupIdWhenIsStarred(
accountId: Int,
text: String,
groupId: String,
isStarred: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId IN (
SELECT id FROM feed WHERE groupId = :groupId
)
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByGroupIdWhenAll(
accountId: Int,
text: String,
groupId: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId = :feedId
AND isUnread = :isUnread
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByFeedIdWhenIsUnread(
accountId: Int,
text: String,
feedId: String,
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId = :feedId
AND isStarred = :isStarred
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByFeedIdWhenIsStarred(
accountId: Int,
text: String,
feedId: String,
isStarred: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND feedId = :feedId
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleByFeedIdWhenAll(
accountId: Int,
text: String,
feedId: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND isUnread = :isUnread
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleWhenIsUnread(
accountId: Int,
text: String,
isUnread: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND isStarred = :isStarred
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleWhenIsStarred(
accountId: Int,
text: String,
isStarred: Boolean,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND (
title LIKE '%' || :text || '%'
OR shortDescription LIKE '%' || :text || '%'
OR fullContent LIKE '%' || :text || '%'
)
ORDER BY date DESC
"""
)
fun searchArticleWhenAll(
accountId: Int,
text: String,
): PagingSource<Int, ArticleWithFeed>
@Query( @Query(
""" """
UPDATE article SET isUnread = :isUnread UPDATE article SET isUnread = :isUnread
@ -92,64 +284,6 @@ interface ArticleDao {
) )
suspend fun deleteByGroupId(accountId: Int, groupId: String) suspend fun deleteByGroupId(accountId: Int, groupId: String)
@Transaction
@Query(
"""
SELECT * FROM article
WHERE accountId = :accountId
AND (
title LIKE :keyword
OR rawDescription LIKE :keyword
OR fullContent LIKE :keyword
)
ORDER BY date DESC
"""
)
fun searchArticleWithFeedWhenIsAll(
accountId: Int,
keyword: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE isUnread = :isUnread
AND accountId = :accountId
AND (
title LIKE :keyword
OR rawDescription LIKE :keyword
OR fullContent LIKE :keyword
)
ORDER BY date DESC
"""
)
fun searchArticleWithFeedWhenIsUnread(
accountId: Int,
isUnread: Boolean,
keyword: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction
@Query(
"""
SELECT * FROM article
WHERE isStarred = :isStarred
AND accountId = :accountId
AND (
title LIKE :keyword
OR rawDescription LIKE :keyword
OR fullContent LIKE :keyword
)
ORDER BY date DESC
"""
)
fun searchArticleWithFeedWhenIsStarred(
accountId: Int,
isStarred: Boolean,
keyword: String,
): PagingSource<Int, ArticleWithFeed>
@Transaction @Transaction
@Query( @Query(
""" """

View File

@ -166,6 +166,41 @@ abstract class AbstractRssRepository constructor(
suspend fun groupMoveToTargetGroup(group: Group, targetGroup: Group) { suspend fun groupMoveToTargetGroup(group: Group, targetGroup: Group) {
feedDao.updateTargetGroupIdByGroupId(context.currentAccountId, group.id, targetGroup.id) feedDao.updateTargetGroupIdByGroupId(context.currentAccountId, group.id, targetGroup.id)
} }
fun searchArticles(
content: String,
groupId: String? = null,
feedId: String? = null,
isStarred: Boolean = false,
isUnread: Boolean = false,
): PagingSource<Int, ArticleWithFeed> {
val accountId = context.currentAccountId
Log.i(
"RLog",
"searchArticles: content: ${content}, accountId: ${accountId}, groupId: ${groupId}, feedId: ${feedId}, isStarred: ${isStarred}, isUnread: ${isUnread}"
)
return when {
groupId != null -> when {
isStarred -> articleDao
.searchArticleByGroupIdWhenIsStarred(accountId, content, groupId, isStarred)
isUnread -> articleDao
.searchArticleByGroupIdWhenIsUnread(accountId, content, groupId, isUnread)
else -> articleDao.searchArticleByGroupIdWhenAll(accountId, content, groupId)
}
feedId != null -> when {
isStarred -> articleDao
.searchArticleByFeedIdWhenIsStarred(accountId, content, feedId, isStarred)
isUnread -> articleDao
.searchArticleByFeedIdWhenIsUnread(accountId, content, feedId, isUnread)
else -> articleDao.searchArticleByFeedIdWhenAll(accountId, content, feedId)
}
else -> when {
isStarred -> articleDao.searchArticleWhenIsStarred(accountId, content, isStarred)
isUnread -> articleDao.searchArticleWhenIsUnread(accountId, content, isUnread)
else -> articleDao.searchArticleWhenAll(accountId, content)
}
}
}
} }
@HiltWorker @HiltWorker

View File

@ -1,5 +1,6 @@
package me.ash.reader.ui.page.home.flow package me.ash.reader.ui.page.home.flow
import androidx.activity.compose.BackHandler
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -18,8 +19,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SmallTopAppBar import androidx.compose.material3.SmallTopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@ -28,6 +31,8 @@ import androidx.navigation.NavHostController
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.work.WorkInfo import androidx.work.WorkInfo
import com.google.accompanist.pager.PagerState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.ash.reader.R import me.ash.reader.R
import me.ash.reader.data.entity.ArticleWithFeed import me.ash.reader.data.entity.ArticleWithFeed
@ -42,6 +47,7 @@ import me.ash.reader.ui.page.home.FilterState
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalFoundationApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class, ExperimentalFoundationApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class,
androidx.compose.ui.ExperimentalComposeUiApi::class,
) )
@Composable @Composable
fun FlowPage( fun FlowPage(
@ -55,10 +61,13 @@ fun FlowPage(
onItemClick: (item: ArticleWithFeed) -> Unit = {}, onItemClick: (item: ArticleWithFeed) -> Unit = {},
) { ) {
val context = LocalContext.current val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var markAsRead by remember { mutableStateOf(false) }
var onSearch by remember { mutableStateOf(false) }
val viewState = flowViewModel.viewState.collectAsStateValue() val viewState = flowViewModel.viewState.collectAsStateValue()
val pagingItems = viewState.pagingData.collectAsLazyPagingItems() val pagingItems = viewState.pagingData.collectAsLazyPagingItems()
var markAsRead by remember { mutableStateOf(false) }
val owner = LocalLifecycleOwner.current val owner = LocalLifecycleOwner.current
var isSyncing by remember { mutableStateOf(false) } var isSyncing by remember { mutableStateOf(false) }
@ -67,9 +76,37 @@ fun FlowPage(
} }
LaunchedEffect(filterState) { LaunchedEffect(filterState) {
flowViewModel.dispatch( snapshotFlow { filterState }.collect {
FlowViewAction.FetchData(filterState) flowViewModel.dispatch(
) FlowViewAction.FetchData(it)
)
}
}
LaunchedEffect(onSearch) {
snapshotFlow { onSearch }.collect {
if (it) {
delay(100) // ???
focusRequester.requestFocus()
} else {
keyboardController?.hide()
if (viewState.searchContent.isNotBlank()) {
flowViewModel.dispatch(FlowViewAction.InputSearchContent(""))
}
}
}
}
LaunchedEffect(viewState.listState) {
snapshotFlow { viewState.listState.firstVisibleItemIndex }.collect {
if (it > 0) {
keyboardController?.hide()
}
}
}
BackHandler(onSearch) {
onSearch = false
} }
Scaffold( Scaffold(
@ -83,12 +120,13 @@ fun FlowPage(
contentDescription = stringResource(R.string.back), contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface tint = MaterialTheme.colorScheme.onSurface
) { ) {
onSearch = false
onScrollToPage(0) onScrollToPage(0)
} }
}, },
actions = { actions = {
AnimatedVisibility( AnimatedVisibility(
visible = !filterState.filter.isStarred(),// && pagingItems.loadState.refresh is LoadState.NotLoading && pagingItems.itemCount != 0, visible = !filterState.filter.isStarred(),
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(), exit = fadeOut() + shrinkVertically(),
) { ) {
@ -104,20 +142,28 @@ fun FlowPage(
scope.launch { scope.launch {
viewState.listState.scrollToItem(0) viewState.listState.scrollToItem(0)
markAsRead = !markAsRead markAsRead = !markAsRead
onSearch = false
} }
} }
} }
FeedbackIconButton( FeedbackIconButton(
imageVector = Icons.Rounded.Search, imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search), contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurface, tint = if (onSearch) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
},
) { ) {
scope.launch {
viewState.listState.scrollToItem(0)
onSearch = !onSearch
}
} }
} }
) )
}, },
content = { content = {
Crossfade(targetState = pagingItems) { pagingItems ->
// if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) { // if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount == 0) {
// LottieAnimation( // LottieAnimation(
// modifier = Modifier // modifier = Modifier
@ -126,57 +172,93 @@ fun FlowPage(
// url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json", // url = "https://assets7.lottiefiles.com/packages/lf20_l4ny0jjm.json",
// ) // )
// } // }
LazyColumn( LazyColumn(
state = viewState.listState, state = viewState.listState,
) { ) {
item { item {
DisplayText( DisplayText(
modifier = Modifier.padding(start = 30.dp), modifier = Modifier.padding(start = 30.dp),
text = when { text = when {
filterState.group != null -> filterState.group.name filterState.group != null -> filterState.group.name
filterState.feed != null -> filterState.feed.name filterState.feed != null -> filterState.feed.name
else -> filterState.filter.getName() else -> filterState.filter.getName()
}, },
desc = if (isSyncing) stringResource(R.string.syncing) else "", desc = if (isSyncing) stringResource(R.string.syncing) else "",
)
}
item {
AnimatedVisibility(
visible = markAsRead,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
) {
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
MarkAsReadBar(
visible = markAsRead,
absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
onDismissRequest = {
markAsRead = false
},
) {
markAsRead = false
flowViewModel.dispatch(
FlowViewAction.MarkAsRead(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
articleId = null,
markAsReadBefore = it,
)
) )
} }
item { }
AnimatedVisibility( item {
visible = markAsRead, AnimatedVisibility(
enter = fadeIn() + expandVertically(), visible = onSearch,
exit = fadeOut() + shrinkVertically(), enter = fadeIn() + expandVertically(),
) { exit = fadeOut() + shrinkVertically(),
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
}
MarkAsReadBar(
visible = markAsRead,
absoluteY = if (isSyncing) (4 + 16 + 180).dp else 180.dp,
onDismissRequest = {
markAsRead = false
},
) {
markAsRead = false
flowViewModel.dispatch(
FlowViewAction.MarkAsRead(
groupId = filterState.group?.id,
feedId = filterState.feed?.id,
articleId = null,
markAsReadBefore = it,
)
)
}
}
generateArticleList(
context = context,
pagingItems = pagingItems,
) { ) {
onItemClick(it) SearchBar(
value = viewState.searchContent,
placeholder = when {
filterState.group != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.group.name
)
filterState.feed != null -> stringResource(
R.string.search_for_in,
filterState.filter.getName(),
filterState.feed.name
)
else -> stringResource(
R.string.search_for,
filterState.filter.getName()
)
},
focusRequester = focusRequester,
onValueChange = {
flowViewModel.dispatch(FlowViewAction.InputSearchContent(it))
},
onClose = {
onSearch = false
flowViewModel.dispatch(FlowViewAction.InputSearchContent(""))
}
)
Spacer(modifier = Modifier.height((56 + 24 + 10).dp))
} }
item { }
generateArticleList(
context = context,
pagingItems = pagingItems,
) {
onSearch = false
onItemClick(it)
}
item {
Spacer(modifier = Modifier.height(64.dp))
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount != 0) {
Spacer(modifier = Modifier.height(64.dp)) Spacer(modifier = Modifier.height(64.dp))
if (pagingItems.loadState.source.refresh is LoadState.NotLoading && pagingItems.itemCount != 0) {
Spacer(modifier = Modifier.height(64.dp))
}
} }
} }
} }
@ -187,7 +269,9 @@ fun FlowPage(
.height(60.dp) .height(60.dp)
.fillMaxWidth(), .fillMaxWidth(),
filter = filterState.filter, filter = filterState.filter,
filterOnClick = { onFilterChange(filterState.copy(filter = it)) }, filterOnClick = {
onFilterChange(filterState.copy(filter = it))
},
) )
} }
) )

View File

@ -35,31 +35,50 @@ class FlowViewModel @Inject constructor(
action.articleId, action.articleId,
action.markAsReadBefore, action.markAsReadBefore,
) )
is FlowViewAction.InputSearchContent -> inputSearchContent(action.content)
} }
} }
private fun fetchData(filterState: FilterState) { private fun fetchData(filterState: FilterState? = null) {
viewModelScope.launch(Dispatchers.Default) { // viewModelScope.launch(Dispatchers.Default) {
rssRepository.get().pullImportant(filterState.filter.isStarred(), true) // rssRepository.get().pullImportant(filterState.filter.isStarred(), true)
.collect { importantList -> // .collect { importantList ->
_viewState.update { // _viewState.update {
it.copy( // it.copy(
filterImportant = importantList.sumOf { it.important }, // filterImportant = importantList.sumOf { it.important },
// )
// }
// }
// }
if (_viewState.value.searchContent.isNotBlank()) {
_viewState.update {
it.copy(
filterState = filterState,
pagingData = Pager(PagingConfig(pageSize = 10)) {
rssRepository.get().searchArticles(
content = _viewState.value.searchContent.trim(),
groupId = _viewState.value.filterState?.group?.id,
feedId = _viewState.value.filterState?.feed?.id,
isStarred = _viewState.value.filterState?.filter?.isStarred() ?: false,
isUnread = _viewState.value.filterState?.filter?.isUnread() ?: false,
) )
} }.flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
} )
} }
_viewState.update { } else if (filterState != null) {
it.copy( _viewState.update {
pagingData = Pager(PagingConfig(pageSize = 10)) { it.copy(
rssRepository.get().pullArticles( filterState = filterState,
groupId = filterState.group?.id, pagingData = Pager(PagingConfig(pageSize = 10)) {
feedId = filterState.feed?.id, rssRepository.get().pullArticles(
isStarred = filterState.filter.isStarred(), groupId = filterState.group?.id,
isUnread = filterState.filter.isUnread(), feedId = filterState.feed?.id,
) isStarred = filterState.filter.isStarred(),
}.flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope) isUnread = filterState.filter.isUnread(),
) )
}.flow.flowOn(Dispatchers.IO).cachedIn(viewModelScope)
)
}
} }
} }
@ -105,14 +124,25 @@ class FlowViewModel @Inject constructor(
) )
} }
} }
private fun inputSearchContent(content: String) {
_viewState.update {
it.copy(
searchContent = content,
)
}
fetchData(_viewState.value.filterState)
}
} }
data class ArticleViewState( data class ArticleViewState(
val filterState: FilterState? = null,
val filterImportant: Int = 0, val filterImportant: Int = 0,
val listState: LazyListState = LazyListState(), val listState: LazyListState = LazyListState(),
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val pagingData: Flow<PagingData<ArticleWithFeed>> = emptyFlow(), val pagingData: Flow<PagingData<ArticleWithFeed>> = emptyFlow(),
val syncWorkInfo: String = "", val syncWorkInfo: String = "",
val searchContent: String = "",
) )
sealed class FlowViewAction { sealed class FlowViewAction {
@ -134,6 +164,10 @@ sealed class FlowViewAction {
val articleId: String?, val articleId: String?,
val markAsReadBefore: MarkAsReadBefore val markAsReadBefore: MarkAsReadBefore
) : FlowViewAction() ) : FlowViewAction()
data class InputSearchContent(
val content: String,
) : FlowViewAction()
} }
enum class MarkAsReadBefore { enum class MarkAsReadBefore {

View File

@ -14,6 +14,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -34,7 +35,9 @@ fun MarkAsReadBar(
val animated = remember { Animatable(absoluteY.value) } val animated = remember { Animatable(absoluteY.value) }
LaunchedEffect(absoluteY) { LaunchedEffect(absoluteY) {
animated.animateTo(absoluteY.value, spring(stiffness = Spring.StiffnessMediumLow)) snapshotFlow { absoluteY }.collect {
animated.animateTo(it.value, spring(stiffness = Spring.StiffnessMediumLow))
}
} }
AnimatedPopup( AnimatedPopup(

View File

@ -0,0 +1,107 @@
package me.ash.reader.ui.page.home.flow
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.unit.dp
import me.ash.reader.R
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
value: String,
placeholder: String = "",
focusRequester: FocusRequester = remember { FocusRequester() },
onValueChange: (String) -> Unit = {},
onClose: () -> Unit = {},
) {
val focusManager = LocalFocusManager.current
Surface(
modifier = Modifier
.height(56.dp)
.padding(horizontal = 24.dp)
.fillMaxWidth(),
shape = CircleShape,
tonalElevation = 3.dp
) {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.padding(start = 16.dp),
imageVector = Icons.Rounded.Search,
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
androidx.compose.material.TextField(
modifier = Modifier
.height(56.dp)
.fillMaxWidth()
.focusRequester(focusRequester),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onSurface,
textColor = MaterialTheme.colorScheme.onSurface,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
),
value = value,
onValueChange = { onValueChange(it) },
placeholder = {
Text(
text = placeholder,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
alpha = 0.7f
),
)
},
textStyle = MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
baselineShift = BaselineShift(0.1f)
),
singleLine = true,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
)
)
}
IconButton(onClick = { onClose() }) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = stringResource(R.string.clear),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}

View File

@ -58,6 +58,8 @@
<string name="today">今天</string> <string name="today">今天</string>
<string name="yesterday">昨天</string> <string name="yesterday">昨天</string>
<string name="date_at_time">%1$s %2$s</string> <string name="date_at_time">%1$s %2$s</string>
<string name="search_for_in">在%1$s的 \"%2$s\" 中搜索</string>
<string name="search_for">在%1$s中搜索</string>
<string name="mark_as_read">标记为已读</string> <string name="mark_as_read">标记为已读</string>
<string name="mark_all_as_read">全部标记为已读</string> <string name="mark_all_as_read">全部标记为已读</string>
<string name="mark_as_unread">标记为未读</string> <string name="mark_as_unread">标记为未读</string>

View File

@ -58,6 +58,8 @@
<string name="today">Today</string> <string name="today">Today</string>
<string name="yesterday">Yesterday</string> <string name="yesterday">Yesterday</string>
<string name="date_at_time">%1$s At %2$s</string> <string name="date_at_time">%1$s At %2$s</string>
<string name="search_for_in">Search for %1$s Items in \"%2$s\"</string>
<string name="search_for">Search for %1$s Items</string>
<string name="mark_as_read">Mark as Read</string> <string name="mark_as_read">Mark as Read</string>
<string name="mark_all_as_read">Mark All as Read</string> <string name="mark_all_as_read">Mark All as Read</string>
<string name="mark_as_unread">Mark as Unread</string> <string name="mark_as_unread">Mark as Unread</string>