feat(Search): 高亮搜索词

This commit is contained in:
HuanCheng65 2024-01-29 16:19:57 +08:00
parent 2cf07d8e62
commit 7087984b57
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
6 changed files with 153 additions and 9 deletions

View File

@ -35,7 +35,7 @@ data class SearchThreadBean(
data class ThreadInfoBean( data class ThreadInfoBean(
val tid: String, val tid: String,
val pid: String, val pid: String,
val cid: String, val cid: String = "0",
val title: String, val title: String,
val content: String, val content: String,
val time: String, val time: String,

View File

@ -326,6 +326,7 @@ fun ForumSearchPostPage(
) )
}, },
hideForum = true, hideForum = true,
searchKeyword = currentKeyword,
) { ) {
stickyHeader(key = "Sort&Filter") { stickyHeader(key = "Sort&Filter") {
Row( Row(

View File

@ -153,6 +153,7 @@ fun SearchThreadPage(
) )
) )
}, },
searchKeyword = keyword,
) )
PullRefreshIndicator( PullRefreshIndicator(

View File

@ -271,6 +271,7 @@ fun ThreadContent(
showAbstract: Boolean = true, showAbstract: Boolean = true,
isGood: Boolean = false, isGood: Boolean = false,
maxLines: Int = 5, maxLines: Int = 5,
highlightKeywords: ImmutableList<String> = persistentListOf(),
) { ) {
val content = buildAnnotatedString { val content = buildAnnotatedString {
if (showTitle) { if (showTitle) {
@ -298,7 +299,7 @@ fun ThreadContent(
} }
} }
EmoticonText( HighlightText(
text = content, text = content,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -308,6 +309,7 @@ fun ThreadContent(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = maxLines, maxLines = maxLines,
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
highlightKeywords = highlightKeywords
) )
} }

View File

@ -71,6 +71,7 @@ fun QuotePostCard(
quotePostInfo: SearchThreadBean.PostInfo, quotePostInfo: SearchThreadBean.PostInfo,
mainPost: SearchThreadBean.MainPost, mainPost: SearchThreadBean.MainPost,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyword: String? = null,
) { ) {
val quoteContentString = remember(quotePostInfo) { val quoteContentString = remember(quotePostInfo) {
buildAnnotatedStringWithUser( buildAnnotatedStringWithUser(
@ -84,13 +85,15 @@ fun QuotePostCard(
modifier = modifier.padding(16.dp), modifier = modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
PbContentText( HighlightText(
text = quoteContentString, text = quoteContentString,
style = MaterialTheme.typography.body2, style = MaterialTheme.typography.body2,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
highlightKeywords = (keyword?.split(" ") ?: emptyList()).toImmutableList()
) )
MainPostCard( MainPostCard(
keyword = keyword,
mainPost = mainPost, mainPost = mainPost,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -104,6 +107,7 @@ fun QuotePostCard(
fun MainPostCard( fun MainPostCard(
mainPost: SearchThreadBean.MainPost, mainPost: SearchThreadBean.MainPost,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
keyword: String? = null,
) { ) {
val titleString = remember(mainPost) { val titleString = remember(mainPost) {
buildAnnotatedStringWithUser( buildAnnotatedStringWithUser(
@ -117,12 +121,13 @@ fun MainPostCard(
modifier = modifier.padding(16.dp), modifier = modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
PbContentText( HighlightText(
text = titleString, text = titleString,
style = MaterialTheme.typography.subtitle2, style = MaterialTheme.typography.subtitle2,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
highlightKeywords = (keyword?.split(" ") ?: emptyList()).toImmutableList()
) )
if (mainPost.content.isNotBlank()) { if (mainPost.content.isNotBlank()) {
PbContentText( PbContentText(
@ -146,6 +151,7 @@ fun SearchThreadList(
onQuotePostClick: (SearchThreadBean.PostInfo) -> Unit = {}, onQuotePostClick: (SearchThreadBean.PostInfo) -> Unit = {},
onMainPostClick: (SearchThreadBean.MainPost) -> Unit = {}, onMainPostClick: (SearchThreadBean.MainPost) -> Unit = {},
hideForum: Boolean = false, hideForum: Boolean = false,
searchKeyword: String? = null,
header: LazyListScope.() -> Unit = {}, header: LazyListScope.() -> Unit = {},
) { ) {
MyLazyColumn( MyLazyColumn(
@ -162,9 +168,10 @@ fun SearchThreadList(
onClick = onItemClick, onClick = onItemClick,
onUserClick = onItemUserClick, onUserClick = onItemUserClick,
onForumClick = onItemForumClick, onForumClick = onItemForumClick,
hideForum = hideForum,
onQuotePostClick = onQuotePostClick, onQuotePostClick = onQuotePostClick,
onMainPostClick = onMainPostClick, onMainPostClick = onMainPostClick,
hideForum = hideForum,
searchKeyword = searchKeyword
) )
} }
} }
@ -215,6 +222,7 @@ fun SearchThreadItem(
onQuotePostClick: (SearchThreadBean.PostInfo) -> Unit = {}, onQuotePostClick: (SearchThreadBean.PostInfo) -> Unit = {},
onMainPostClick: (SearchThreadBean.MainPost) -> Unit = {}, onMainPostClick: (SearchThreadBean.MainPost) -> Unit = {},
hideForum: Boolean = false, hideForum: Boolean = false,
searchKeyword: String? = null,
) { ) {
Card( Card(
modifier = modifier, modifier = modifier,
@ -232,8 +240,8 @@ fun SearchThreadItem(
showTitle = item.mainPost == null && item.title.isNotBlank(), showTitle = item.mainPost == null && item.title.isNotBlank(),
showAbstract = item.content.isNotBlank(), showAbstract = item.content.isNotBlank(),
maxLines = 2, maxLines = 2,
highlightKeywords = (searchKeyword?.split(" ") ?: emptyList()).toImmutableList(),
) )
SearchMedia(medias = item.media.toImmutableList())
if (item.mainPost != null) { if (item.mainPost != null) {
if (item.postInfo != null) { if (item.postInfo != null) {
QuotePostCard( QuotePostCard(
@ -245,7 +253,8 @@ fun SearchThreadItem(
.background(ExtendedTheme.colors.floorCard) .background(ExtendedTheme.colors.floorCard)
.clickable { .clickable {
onQuotePostClick(item.postInfo) onQuotePostClick(item.postInfo)
} },
keyword = searchKeyword
) )
} else { } else {
MainPostCard( MainPostCard(
@ -256,9 +265,12 @@ fun SearchThreadItem(
.background(ExtendedTheme.colors.floorCard) .background(ExtendedTheme.colors.floorCard)
.clickable { .clickable {
onMainPostClick(item.mainPost) onMainPostClick(item.mainPost)
} },
keyword = searchKeyword
) )
} }
} else {
SearchMedia(medias = item.media.toImmutableList())
} }
if (!hideForum && item.forumName.isNotEmpty()) { if (!hideForum && item.forumName.isNotEmpty()) {
ForumInfoChip( ForumInfoChip(

View File

@ -32,11 +32,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -54,10 +54,12 @@ import com.huanchengfly.tieba.post.pxToDp
import com.huanchengfly.tieba.post.pxToSp import com.huanchengfly.tieba.post.pxToSp
import com.huanchengfly.tieba.post.pxToSpFloat import com.huanchengfly.tieba.post.pxToSpFloat
import com.huanchengfly.tieba.post.spToPxFloat import com.huanchengfly.tieba.post.spToPxFloat
import com.huanchengfly.tieba.post.ui.common.PbContentText
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.utils.EmoticonManager import com.huanchengfly.tieba.post.utils.EmoticonManager
import com.huanchengfly.tieba.post.utils.EmoticonUtil.emoticonString import com.huanchengfly.tieba.post.utils.EmoticonUtil.emoticonString
import com.huanchengfly.tieba.post.utils.calcLineHeightPx import com.huanchengfly.tieba.post.utils.calcLineHeightPx
import java.util.regex.Pattern
@Composable @Composable
fun EmoticonText( fun EmoticonText(
@ -399,3 +401,129 @@ fun buildChipInlineContent(
} }
) )
} }
@Composable
fun HighlightText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
lineSpacing: TextUnit = 0.sp,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
emoticonSize: Float = 0.9f,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
highlightKeywords: List<String> = emptyList(),
highlightColor: Color = ExtendedTheme.colors.primary,
highlightStyle: TextStyle = style,
) {
HighlightText(
text = AnnotatedString(text),
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
lineSpacing = lineSpacing,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
emoticonSize = emoticonSize,
onTextLayout = onTextLayout,
style = style,
highlightKeywords = highlightKeywords,
highlightColor = highlightColor,
highlightStyle = highlightStyle,
)
}
@Composable
fun HighlightText(
text: AnnotatedString,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
lineSpacing: TextUnit = 0.sp,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
emoticonSize: Float = 0.9f,
inlineContent: Map<String, InlineTextContent> = emptyMap(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
highlightKeywords: List<String> = emptyList(),
highlightColor: Color = ExtendedTheme.colors.primary,
highlightStyle: TextStyle = style,
) {
val mergedHighlightStyle = remember(highlightStyle, highlightColor) {
highlightStyle.copy(color = highlightColor)
}
val highlightText = remember(text, highlightKeywords) {
if (highlightKeywords.isEmpty()) {
text
} else {
buildAnnotatedString {
append(text)
highlightKeywords.forEach { keyword ->
val regexPattern = keyword.toPattern(Pattern.CASE_INSENSITIVE)
val matcher = regexPattern.matcher(text.text)
while (matcher.find()) {
val start = matcher.start()
val end = matcher.end()
addStyle(
mergedHighlightStyle.toSpanStyle(),
start,
end
)
}
}
}
}
}
PbContentText(
text = highlightText,
modifier = modifier,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
lineSpacing = lineSpacing,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
emoticonSize = emoticonSize,
inlineContent = inlineContent,
onTextLayout = onTextLayout,
style = style,
)
}