Add article search feature
This commit is contained in:
parent
aaf032332b
commit
25009e9036
|
@ -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(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
107
app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt
Normal file
107
app/src/main/java/me/ash/reader/ui/page/home/flow/SearchBar.kt
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user