[优化] 优化书架搜索筛选逻辑,可以在书架筛选书源,现在输入关键词时回车触发搜索

This commit is contained in:
HapeLee
2026-04-23 13:21:37 +08:00
parent f8473077dc
commit 2c63332403
12 changed files with 285 additions and 82 deletions
@@ -75,6 +75,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -107,6 +108,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -136,6 +138,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -165,6 +168,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -199,6 +203,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -234,6 +239,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -264,6 +270,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -284,7 +291,9 @@ interface BookDao {
) )
fun flowBookShelfByUserGroup(group: Long): Flow<List<BookShelfItem>> fun flowBookShelfByUserGroup(group: Long): Flow<List<BookShelfItem>>
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") @Query(
"SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'"
)
fun flowSearch(key: String): Flow<List<Book>> fun flowSearch(key: String): Flow<List<Book>>
@Query( @Query(
@@ -293,6 +302,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -308,7 +318,7 @@ interface BookDao {
`order`, `order`,
canUpdate canUpdate
FROM books FROM books
WHERE name like '%'||:key||'%' or author like '%'||:key||'%' WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
""" """
) )
fun flowBookShelfSearch(key: String): Flow<List<BookShelfItem>> fun flowBookShelfSearch(key: String): Flow<List<BookShelfItem>>
@@ -322,6 +332,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -352,6 +363,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -381,6 +393,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -410,6 +423,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -439,6 +453,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -468,6 +483,7 @@ interface BookDao {
bookUrl, bookUrl,
name, name,
author, author,
originName,
coverUrl, coverUrl,
customCoverUrl, customCoverUrl,
durChapterTitle, durChapterTitle,
@@ -13,6 +13,7 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.localBook.TextFile import io.legado.app.model.localBook.TextFile
@@ -274,7 +275,7 @@ object BookHelp {
book: Book, book: Book,
bookChapter: BookChapter, bookChapter: BookChapter,
content: String, content: String,
concurrency: Int = AppConfig.threadCount concurrency: Int = OtherConfig.threadCount
) = coroutineScope { ) = coroutineScope {
flowImages(bookChapter, content).onEachParallel(concurrency) { mSrc -> flowImages(bookChapter, content).onEachParallel(concurrency) { mSrc ->
saveImage(bookSource, book, mSrc, bookChapter) saveImage(bookSource, book, mSrc, bookChapter)
@@ -19,6 +19,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.isTrue import io.legado.app.utils.isTrue
import io.legado.app.utils.mapAsync import io.legado.app.utils.mapAsync
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
@@ -96,7 +97,7 @@ object BookChapterList {
for (urlStr in chapterData.second) { for (urlStr in chapterData.second) {
emit(urlStr) emit(urlStr)
} }
}.mapAsync(AppConfig.threadCount) { urlStr -> }.mapAsync(OtherConfig.threadCount) { urlStr ->
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
mUrl = urlStr, mUrl = urlStr,
source = bookSource, source = bookSource,
@@ -19,6 +19,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setNextChapterUrl
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.utils.HtmlFormatter import io.legado.app.utils.HtmlFormatter
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.mapAsync import io.legado.app.utils.mapAsync
@@ -105,7 +106,7 @@ object BookContent {
for (urlStr in contentData.second) { for (urlStr in contentData.second) {
emit(urlStr) emit(urlStr)
} }
}.mapAsync(AppConfig.threadCount) { urlStr -> }.mapAsync(OtherConfig.threadCount) { urlStr ->
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
mUrl = urlStr, mUrl = urlStr,
source = bookSource, source = bookSource,
@@ -241,7 +241,7 @@ private fun CacheScreen(
) { dismiss -> ) { dismiss ->
state.groupList.forEach { group -> state.groupList.forEach { group ->
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = "${stringResource(R.string.group)}: ${group.groupName}", text = "${group.groupName}",
isSelected = group.groupId == state.groupId, isSelected = group.groupId == state.groupId,
onClick = { onClick = {
dismiss() dismiss()
@@ -394,6 +394,8 @@ fun BookItem(
titleCenter: Boolean = true, titleCenter: Boolean = true,
titleMaxLines: Int = 2, titleMaxLines: Int = 2,
coverShadow: Boolean = false, coverShadow: Boolean = false,
isSearchMode: Boolean = false,
searchKey: String = "",
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit
) { ) {
@@ -409,6 +411,15 @@ fun BookItem(
} else { } else {
null null
} }
val matchedSourceLabel = if (
isSearchMode &&
searchKey.isNotBlank() &&
book.originName.contains(searchKey, ignoreCase = true)
) {
book.originName
} else {
null
}
BookshelfItem( BookshelfItem(
isGrid = layoutMode != 0, isGrid = layoutMode != 0,
@@ -425,7 +436,7 @@ fun BookItem(
modifier = modifier, modifier = modifier,
badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null, badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew, showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,
leftBottomText = bookTypeLabel leftBottomText = matchedSourceLabel ?: bookTypeLabel
) )
}, },
title = book.name, title = book.name,
@@ -9,6 +9,7 @@ data class BookShelfItem(
val bookUrl: String, val bookUrl: String,
val name: String, val name: String,
val author: String, val author: String,
val originName: String,
val coverUrl: String?, val coverUrl: String?,
val customCoverUrl: String?, val customCoverUrl: String?,
val durChapterTitle: String?, val durChapterTitle: String?,
@@ -69,9 +69,7 @@ import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.about.AppLogSheet import io.legado.app.ui.about.AppLogSheet
import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.info.GroupSelectSheet import io.legado.app.ui.book.info.GroupSelectSheet
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
@@ -82,6 +80,7 @@ import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.button.TopBarActionButton import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.card.NormalCard import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -92,7 +91,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.tabRow.AppTabRow
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.readText import io.legado.app.utils.readText
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -109,8 +107,10 @@ fun BookshelfScreen(
viewModel: BookshelfViewModel = koinViewModel(), viewModel: BookshelfViewModel = koinViewModel(),
onBookClick: (BookShelfItem) -> Unit, onBookClick: (BookShelfItem) -> Unit,
onBookLongClick: (BookShelfItem) -> Unit, onBookLongClick: (BookShelfItem) -> Unit,
onNavigateToSearch: (String) -> Unit,
onNavigateToRemoteImport: () -> Unit, onNavigateToRemoteImport: () -> Unit,
onNavigateToLocalImport: () -> Unit onNavigateToLocalImport: () -> Unit,
onNavigateToCache: (Long) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@@ -182,8 +182,8 @@ fun BookshelfScreen(
pageCount = { uiState.groups.size } pageCount = { uiState.groups.size }
) )
LaunchedEffect(uiState.groups) { LaunchedEffect(uiState.groups, uiState.isSearch) {
if (uiState.groups.isNotEmpty()) { if (!uiState.isSearch && uiState.groups.isNotEmpty()) {
val savedGroupId = BookshelfConfig.saveTabPosition val savedGroupId = BookshelfConfig.saveTabPosition
val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId } val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) { if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
@@ -208,8 +208,16 @@ fun BookshelfScreen(
} }
} }
val currentGroupId = val currentTabGroupId =
uiState.groups.getOrNull(pagerState.currentPage)?.groupId ?: BookGroup.IdAll uiState.groups.getOrNull(pagerState.currentPage)?.groupId ?: BookGroup.IdAll
val searchGroupExists = uiState.allGroups.any { it.groupId == uiState.selectedGroupId }
val currentGroupId = if (uiState.isSearch && searchGroupExists) {
uiState.selectedGroupId
} else {
currentTabGroupId
}
val isUsingStandaloneSearchGroup = uiState.isSearch &&
uiState.groups.none { it.groupId == currentGroupId }
val currentGroupBookCountFlow = remember(currentGroupId) { val currentGroupBookCountFlow = remember(currentGroupId) {
appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size } appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size }
} }
@@ -266,28 +274,33 @@ fun BookshelfScreen(
} }
} }
val baseTitle = when (bookGroupStyle) { val currentGroupName = uiState.allGroups.firstOrNull { it.groupId == currentGroupId }?.groupName
1 -> { ?: uiState.groups.getOrNull(pagerState.currentPage)?.groupName
uiState.groups.getOrNull(pagerState.currentPage)?.groupName
?: stringResource(R.string.bookshelf) val baseTitle = when {
} uiState.isSearch && bookGroupStyle == 0 -> stringResource(R.string.bookshelf)
2 if uiState.groups.isNotEmpty() -> { uiState.isSearch -> currentGroupName ?: stringResource(R.string.bookshelf)
bookGroupStyle == 1 -> currentGroupName ?: stringResource(R.string.bookshelf)
bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
if (isInFolderRoot) stringResource(R.string.bookshelf) if (isInFolderRoot) stringResource(R.string.bookshelf)
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName else currentGroupName ?: stringResource(R.string.bookshelf)
?: stringResource(R.string.bookshelf)
} }
else -> stringResource(R.string.bookshelf) else -> stringResource(R.string.bookshelf)
} }
val title = if (isEditMode) { val title = if (isEditMode) {
"$baseTitle ${selectedBookUrls.size} / $currentGroupBookCount" "$currentGroupBookCount · ${selectedBookUrls.size} "
} else if (uiState.upBooksCount > 0) { } else if (uiState.upBooksCount > 0) {
"$baseTitle (${uiState.upBooksCount})" "$baseTitle (${uiState.upBooksCount})"
} else { } else {
baseTitle baseTitle
} }
val subtitle = if (isEditMode) { val subtitle = if (isEditMode) {
"${stringResource(R.string.all)}${stringResource(R.string.book_count, allGroupsBookCount)}" if (bookGroupStyle == 0) {
"${allGroupsBookCount}"
} else {
"${currentGroupName ?: stringResource(R.string.bookshelf)} · 共${allGroupsBookCount}"
}
} else { } else {
null null
} }
@@ -317,9 +330,28 @@ fun BookshelfScreen(
state = uiState, state = uiState,
onBackClick = if (isEditMode) exitEditMode else null, onBackClick = if (isEditMode) exitEditMode else null,
backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back, backNavigationIcon = if (isEditMode) AppIcons.Close else AppIcons.Back,
showSearchAction = !isEditMode, showSearchAction = true,
onSearchToggle = { context.startActivity<SearchActivity>() }, onSearchToggle = { active ->
viewModel.setSearchMode(active)
if (!active && uiState.selectedGroupId != currentTabGroupId) {
viewModel.changeGroup(currentTabGroupId)
}
},
onSearchQueryChange = { viewModel.setSearchKey(it) }, onSearchQueryChange = { viewModel.setSearchKey(it) },
onSearchSubmit = { rawQuery ->
rawQuery.trim()
.takeIf { it.isNotEmpty() }
?.let(onNavigateToSearch)
},
searchTrailingIcon = {
if (uiState.searchKey.isNotEmpty()) {
TopBarActionButton(
onClick = { viewModel.setSearchKey("") },
imageVector = AppIcons.Close,
contentDescription = stringResource(R.string.clear)
)
}
},
topBarActions = { topBarActions = {
if (isEditMode) { if (isEditMode) {
TopBarActionButton( TopBarActionButton(
@@ -393,9 +425,7 @@ fun BookshelfScreen(
onClick = { onClick = {
val groupId = val groupId =
uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
context.startActivity<CacheActivity> { onNavigateToCache(groupId)
putExtra("groupId", groupId)
}
dismiss() dismiss()
}, },
leadingIcon = { Icon(Icons.Default.Download, null) } leadingIcon = { Icon(Icons.Default.Download, null) }
@@ -463,11 +493,70 @@ fun BookshelfScreen(
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = group.groupName, text = group.groupName,
onClick = { onClick = {
if (uiState.isSearch) {
viewModel.changeGroup(group.groupId)
}
scope.launch { pagerState.animateScrollToPage(index) } scope.launch { pagerState.animateScrollToPage(index) }
dismiss() dismiss()
}, },
trailingIcon = { trailingIcon = {
if (selectedTabIndex == index) { val isSelected = if (uiState.isSearch) {
uiState.selectedGroupId == group.groupId
} else {
selectedTabIndex == index
}
if (isSelected) {
Icon(
Icons.Default.Check,
null,
modifier = Modifier.size(18.dp)
)
}
}
)
}
if (uiState.isSearch) {
val allGroup = uiState.allGroups.firstOrNull {
it.groupId == BookGroup.IdAll
}
val hiddenGroups = uiState.allGroups.filter {
!it.show && it.groupId != BookGroup.IdAll
}
if (allGroup != null || hiddenGroups.isNotEmpty()) {
PillHeaderDivider(
title = "${stringResource(R.string.all)} / ${stringResource(R.string.hide)}"
)
allGroup?.let { group ->
RoundDropdownMenuItem(
text = group.groupName,
onClick = {
viewModel.changeGroup(group.groupId)
dismiss()
},
trailingIcon = {
if (uiState.selectedGroupId == group.groupId) {
Icon(
Icons.Default.Check,
null,
modifier = Modifier.size(18.dp)
)
}
}
)
}
hiddenGroups.forEach { group ->
RoundDropdownMenuItem(
text = group.groupName,
onClick = {
viewModel.changeGroup(group.groupId)
dismiss()
},
trailingIcon = {
if (uiState.selectedGroupId == group.groupId) {
Icon( Icon(
Icons.Default.Check, Icons.Default.Check,
null, null,
@@ -483,11 +572,17 @@ fun BookshelfScreen(
} }
} }
} }
}
}
} else null } else null
) { paddingValues -> ) { paddingValues ->
var isRefreshing by remember { mutableStateOf(false) } var isRefreshing by remember { mutableStateOf(false) }
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberPullToRefreshState()
val currentGroup = uiState.groups.getOrNull(pagerState.currentPage) val currentGroup = if (uiState.isSearch) {
uiState.allGroups.firstOrNull { it.groupId == currentGroupId }
} else {
uiState.groups.getOrNull(pagerState.currentPage)
}
val pullToRefreshEnabled = (currentGroup?.enableRefresh ?: true) && !isEditMode val pullToRefreshEnabled = (currentGroup?.enableRefresh ?: true) && !isEditMode
Box( Box(
@@ -511,7 +606,7 @@ fun BookshelfScreen(
targetState = isInFolderRoot, targetState = isInFolderRoot,
label = "FolderTransition" label = "FolderTransition"
) { isRoot -> ) { isRoot ->
if (bookGroupStyle == 2 && isRoot) { if (bookGroupStyle == 2 && isRoot && !isUsingStandaloneSearchGroup) {
val folderColumns = val folderColumns =
if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
val isGridMode = bookshelfLayoutMode != 0 val isGridMode = bookshelfLayoutMode != 0
@@ -575,6 +670,22 @@ fun BookshelfScreen(
} }
} }
} }
} else {
if (isUsingStandaloneSearchGroup) {
BookshelfPage(
paddingValues = paddingValues,
books = uiState.items,
uiState = uiState,
bookshelfLayoutMode = bookshelfLayoutMode,
bookshelfLayoutGrid = bookshelfLayoutGrid,
bookshelfLayoutList = bookshelfLayoutList,
isEditMode = isEditMode,
selectedBookUrls = selectedBookUrls,
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
onBookClick = onBookClick,
onBookLongClick = onBookLongClick
)
} else { } else {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
@@ -598,6 +709,7 @@ fun BookshelfScreen(
isEditMode = isEditMode, isEditMode = isEditMode,
selectedBookUrls = selectedBookUrls, selectedBookUrls = selectedBookUrls,
onToggleBookSelection = { toggleBookSelection(it.bookUrl) }, onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
onBookClick = onBookClick, onBookClick = onBookClick,
onBookLongClick = onBookLongClick onBookLongClick = onBookLongClick
) )
@@ -605,6 +717,7 @@ fun BookshelfScreen(
} }
} }
} }
}
PullToRefreshDefaults.LoadingIndicator( PullToRefreshDefaults.LoadingIndicator(
state = pullToRefreshState, state = pullToRefreshState,
@@ -716,10 +829,24 @@ fun BookshelfPage(
isEditMode: Boolean, isEditMode: Boolean,
selectedBookUrls: Set<String>, selectedBookUrls: Set<String>,
onToggleBookSelection: (BookShelfItem) -> Unit, onToggleBookSelection: (BookShelfItem) -> Unit,
onGlobalSearch: () -> Unit,
onBookClick: (BookShelfItem) -> Unit, onBookClick: (BookShelfItem) -> Unit,
onBookLongClick: (BookShelfItem) -> Unit onBookLongClick: (BookShelfItem) -> Unit
) { ) {
if (books.isEmpty()) { if (books.isEmpty()) {
if (uiState.isSearch) {
EmptyMessage(
modifier = Modifier
.fillMaxSize()
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding()
),
message = "没有书籍,尝试全局搜索",
buttonText = "全局搜索",
onButtonClick = onGlobalSearch
)
} else {
EmptyMessage( EmptyMessage(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -729,6 +856,7 @@ fun BookshelfPage(
), ),
messageResId = R.string.bookshelf_empty messageResId = R.string.bookshelf_empty
) )
}
return return
} }
@@ -766,6 +894,8 @@ fun BookshelfPage(
titleCenter = BookshelfConfig.bookshelfTitleCenter, titleCenter = BookshelfConfig.bookshelfTitleCenter,
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines, titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
coverShadow = BookshelfConfig.bookshelfCoverShadow, coverShadow = BookshelfConfig.bookshelfCoverShadow,
isSearchMode = uiState.isSearch,
searchKey = uiState.searchKey,
onClick = { onClick = {
if (isEditMode) { if (isEditMode) {
onToggleBookSelection(book) onToggleBookSelection(book)
@@ -10,9 +10,11 @@ data class BookshelfUiState(
override val isSearch: Boolean = false, override val isSearch: Boolean = false,
override val isLoading: Boolean = false, override val isLoading: Boolean = false,
val groups: List<BookGroup> = emptyList(), val groups: List<BookGroup> = emptyList(),
val allGroups: List<BookGroup> = emptyList(),
val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(), val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
val groupBookCounts: Map<Long, Int> = emptyMap(), val groupBookCounts: Map<Long, Int> = emptyMap(),
val selectedGroupIndex: Int = 0, val selectedGroupIndex: Int = 0,
val selectedGroupId: Long = BookGroup.IdAll,
val loadingText: String? = null, val loadingText: String? = null,
val upBooksCount: Int = 0, val upBooksCount: Int = 0,
val updatingBooks: Set<String> = emptySet() val updatingBooks: Set<String> = emptySet()
@@ -97,6 +97,7 @@ class BookshelfViewModel(
private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition) private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
private val searchKeyFlow = MutableStateFlow("") private val searchKeyFlow = MutableStateFlow("")
private val searchModeFlow = MutableStateFlow(false)
private val refreshTrigger = MutableStateFlow(0) private val refreshTrigger = MutableStateFlow(0)
private val loadingTextFlow = MutableStateFlow<String?>(null) private val loadingTextFlow = MutableStateFlow<String?>(null)
@@ -207,19 +208,27 @@ class BookshelfViewModel(
} }
}.distinctUntilChanged().flowOn(Dispatchers.Default) }.distinctUntilChanged().flowOn(Dispatchers.Default)
private val internalStateFlow = combine( private val coreInternalStateFlow = combine(
groupIdFlow, groupIdFlow,
searchKeyFlow, searchKeyFlow,
searchModeFlow,
loadingTextFlow, loadingTextFlow,
updatingBooksFlow, updatingBooksFlow
) { groupId, searchKey, isSearchMode, loadingText, updatingBooks ->
InternalState(groupId, searchKey, isSearchMode, loadingText, updatingBooks, 0)
}
private val internalStateFlow = combine(
coreInternalStateFlow,
upBooksCountFlow upBooksCountFlow
) { groupId, searchKey, loadingText, updatingBooks, upBooksCount -> ) { core, upBooksCount ->
InternalState(groupId, searchKey, loadingText, updatingBooks, upBooksCount) core.copy(upBooksCount = upBooksCount)
} }
data class InternalState( data class InternalState(
val groupId: Long, val groupId: Long,
val searchKey: String, val searchKey: String,
val isSearchMode: Boolean,
val loadingText: String?, val loadingText: String?,
val updatingBooks: Set<String>, val updatingBooks: Set<String>,
val upBooksCount: Int val upBooksCount: Int
@@ -228,29 +237,27 @@ class BookshelfViewModel(
val uiState: StateFlow<BookshelfUiState> = combine( val uiState: StateFlow<BookshelfUiState> = combine(
booksFlow, booksFlow,
groupsFlow, groupsFlow,
allGroupsFlow,
groupPreviewsFlow, groupPreviewsFlow,
internalStateFlow internalStateFlow
) { books, groups, previews, internal -> ) { books, groups, allGroups, previews, internal ->
val filteredBooks = if (internal.searchKey.isEmpty()) { val filteredBooks = if (!internal.isSearchMode || internal.searchKey.isBlank()) {
books books
} else { } else {
books.filter { books.filter { it.matchesSearchKey(internal.searchKey) }
it.name.contains(
internal.searchKey,
true
) || it.author.contains(internal.searchKey, true)
}
} }
BookshelfUiState( BookshelfUiState(
items = filteredBooks, items = filteredBooks,
groups = groups, groups = groups,
allGroups = allGroups,
groupPreviews = previews.previews, groupPreviews = previews.previews,
groupBookCounts = previews.counts, groupBookCounts = previews.counts,
selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId } selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
.coerceAtLeast(0), .coerceAtLeast(0),
selectedGroupId = internal.groupId,
searchKey = internal.searchKey, searchKey = internal.searchKey,
isSearch = internal.searchKey.isNotEmpty(), isSearch = internal.isSearchMode,
isLoading = internal.loadingText != null, isLoading = internal.loadingText != null,
loadingText = internal.loadingText, loadingText = internal.loadingText,
upBooksCount = internal.upBooksCount, upBooksCount = internal.upBooksCount,
@@ -333,16 +340,15 @@ class BookshelfViewModel(
return combine( return combine(
appDb.bookDao.flowBookShelfByGroup(groupId), appDb.bookDao.flowBookShelfByGroup(groupId),
searchKeyFlow, searchKeyFlow,
searchModeFlow,
groupsFlow, groupsFlow,
refreshTrigger refreshTrigger
) { books, searchKey, groups, _ -> ) { books, searchKey, isSearchMode, groups, _ ->
val group = groups.find { it.groupId == groupId } val group = groups.find { it.groupId == groupId }
val filtered = if (searchKey.isEmpty()) { val filtered = if (!isSearchMode || searchKey.isBlank()) {
books books
} else { } else {
books.filter { books.filter { it.matchesSearchKey(searchKey) }
it.name.contains(searchKey, true) || it.author.contains(searchKey, true)
}
} }
sortBooks(filtered, group) sortBooks(filtered, group)
}.distinctUntilChanged().flowOn(Dispatchers.Default) }.distinctUntilChanged().flowOn(Dispatchers.Default)
@@ -359,6 +365,13 @@ class BookshelfViewModel(
searchKeyFlow.value = key searchKeyFlow.value = key
} }
fun setSearchMode(active: Boolean) {
searchModeFlow.value = active
if (!active) {
searchKeyFlow.value = ""
}
}
fun refresh() { fun refresh() {
refreshTrigger.value++ refreshTrigger.value++
} }
@@ -764,4 +777,10 @@ class BookshelfViewModel(
} }
} }
private fun BookShelfItem.matchesSearchKey(searchKey: String): Boolean {
return name.contains(searchKey, true) ||
author.contains(searchKey, true) ||
originName.contains(searchKey, true)
}
} }
@@ -18,12 +18,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
import io.legado.app.ui.widget.components.text.AnimatedTextLine import io.legado.app.ui.widget.components.text.AnimatedTextLine
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -32,6 +35,9 @@ fun EmptyMessage(
message: String, message: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isLoading: Boolean = false, isLoading: Boolean = false,
buttonText: String? = null,
buttonImageVector: ImageVector = AppIcons.Search,
onButtonClick: (() -> Unit)? = null,
faces: List<String> = listOf( faces: List<String> = listOf(
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)", "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)" "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
@@ -77,6 +83,15 @@ fun EmptyMessage(
softWrap = true, softWrap = true,
modifier = Modifier.widthIn(max = 240.dp) modifier = Modifier.widthIn(max = 240.dp)
) )
if (buttonText != null && onButtonClick != null) {
Spacer(modifier = Modifier.height(8.dp))
SmallTonalTextButton(
onClick = onButtonClick,
text = buttonText,
imageVector = buttonImageVector
)
}
} }
} }
@@ -85,6 +100,9 @@ fun EmptyMessage(
@StringRes messageResId: Int, @StringRes messageResId: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isLoading: Boolean = false, isLoading: Boolean = false,
buttonText: String? = null,
buttonImageVector: ImageVector = AppIcons.Search,
onButtonClick: (() -> Unit)? = null,
faces: List<String> = listOf( faces: List<String> = listOf(
"(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)", "(;′⌒`)", "(つ﹏⊂)", "(•̀ᴗ•́)و", "(๑•́ ₃ •̀๑)",
"(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)" "(눈‸눈)", "(ಥ﹏ಥ)", "(。•́︿•̀。)"
@@ -97,6 +115,9 @@ fun EmptyMessage(
message = message, message = message,
modifier = modifier, modifier = modifier,
isLoading = isLoading, isLoading = isLoading,
buttonText = buttonText,
buttonImageVector = buttonImageVector,
onButtonClick = onButtonClick,
faces = faces, faces = faces,
faceTextSize = faceTextSize, faceTextSize = faceTextSize,
onFaceClick = onFaceClick onFaceClick = onFaceClick
@@ -169,7 +169,7 @@ fun RoundDropdownMenuItem(
Text( Text(
modifier = Modifier.widthIn(max = 200.dp), modifier = Modifier.widthIn(max = 200.dp),
text = text, text = text,
style = LegadoTheme.typography.bodyMediumEmphasized, style = LegadoTheme.typography.labelLargeEmphasized,
color = contentColor color = contentColor
) )
} }