[优化] 优化书架性能

This commit is contained in:
HapeLee
2026-03-17 00:50:59 +08:00
parent 8c46d2fd27
commit c469cab5d6
5 changed files with 116 additions and 55 deletions
@@ -276,12 +276,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefInt(PreferKey.bookshelfLayoutGridLandscape, value)
}
var saveTabPosition: Int
get() = appCtx.getPrefInt(PreferKey.saveTabPosition, 0)
set(value) {
appCtx.putPrefInt(PreferKey.saveTabPosition, value)
}
var bookExportFileName: String?
get() = appCtx.getPrefString(PreferKey.bookExportFileName)
set(value) {
@@ -2,6 +2,7 @@ package io.legado.app.ui.config.bookshelfConfig
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.config.prefDelegate
import io.legado.app.utils.postEvent
@@ -117,4 +118,9 @@ object BookshelfConfig {
*/
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
/**
* 保存上次停留的分组Id
*/
var saveTabPosition by prefDelegate(PreferKey.saveTabPosition, BookGroup.IdAll)
}
@@ -32,7 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
@@ -50,7 +50,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -66,6 +65,7 @@ import io.legado.app.ui.book.manage.BookshelfManageActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.cover.BookCoverWithProgress
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -131,11 +131,19 @@ fun BookshelfScreen(
val bookGroupStyle = BookshelfConfig.bookGroupStyle
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
val title = if (bookGroupStyle == 2 && uiState.groups.isNotEmpty()) {
if (isInFolderRoot) "书架"
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: "书架"
} else {
"书架"
val title = when {
bookGroupStyle == 1 -> {
uiState.groups.getOrNull(pagerState.currentPage)?.groupName
?: stringResource(R.string.bookshelf)
}
bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
if (isInFolderRoot) stringResource(R.string.bookshelf)
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName
?: stringResource(R.string.bookshelf)
}
else -> stringResource(R.string.bookshelf)
}
if (bookGroupStyle == 2 && !isInFolderRoot) {
@@ -228,11 +236,12 @@ fun BookshelfScreen(
bottomContent = if (bookGroupStyle == 0) {
{
if (uiState.groups.isNotEmpty()) {
SecondaryScrollableTabRow(
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
edgePadding = 16.dp,
containerColor = Color.Transparent,
divider = {}
edgePadding = 0.dp,
divider = { },
containerColor = GlassTopAppBarDefaults.containerColor(),
minTabWidth = 0.dp
) {
uiState.groups.forEachIndexed { index, group ->
Tab(
@@ -240,7 +249,15 @@ fun BookshelfScreen(
onClick = {
scope.launch { pagerState.animateScrollToPage(index) }
},
text = { Text(group.groupName) }
text = {
Text(
text = group.groupName,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
@@ -340,16 +357,22 @@ fun BookshelfScreen(
} else {
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxSize(),
beyondViewportPageCount = 1
) {
BookshelfPage(
paddingValues = paddingValues,
uiState = uiState,
onBookClick = onBookClick,
onBookLongClick = onBookLongClick
)
modifier = Modifier.fillMaxSize(),
beyondViewportPageCount = 1,
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
) { pageIndex ->
val group = uiState.groups.getOrNull(pageIndex)
if (group != null) {
val books by viewModel.getBooksFlow(group.groupId)
.collectAsState(emptyList())
BookshelfPage(
paddingValues = paddingValues,
books = books,
uiState = uiState,
onBookClick = onBookClick,
onBookLongClick = onBookLongClick
)
}
}
}
}
@@ -419,6 +442,7 @@ fun BookshelfScreen(
@Composable
fun BookshelfPage(
paddingValues: PaddingValues,
books: List<Book>,
uiState: BookshelfUiState,
onBookClick: (Book) -> Unit,
onBookLongClick: (Book) -> Unit
@@ -431,7 +455,7 @@ fun BookshelfPage(
bottom = 56.dp
)
) {
items(uiState.items, key = { it.bookUrl }) { book ->
items(books, key = { it.bookUrl }) { book ->
BookItem(
book = book,
layoutMode = uiState.bookshelfLayoutMode,
@@ -455,7 +479,7 @@ fun BookshelfPage(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(uiState.items, key = { it.bookUrl }) { book ->
items(books, key = { it.bookUrl }) { book ->
BookItem(
book = book,
layoutMode = uiState.bookshelfLayoutMode,
@@ -54,6 +54,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -61,6 +62,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
@@ -84,7 +86,7 @@ import kotlin.math.min
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
var addBookJob: Coroutine<*>? = null
private val groupIdFlow = MutableStateFlow(BookGroup.IdAll)
private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
private val searchKeyFlow = MutableStateFlow("")
private val refreshTrigger = MutableStateFlow(0)
private val loadingTextFlow = MutableStateFlow<String?>(null)
@@ -102,16 +104,35 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val groupsFlow = appDb.bookGroupDao.show.asFlow()
val groupsFlow: StateFlow<List<BookGroup>> = appDb.bookGroupDao.show.asFlow()
.map { it ?: emptyList() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
private val allBooksFlow = appDb.bookDao.flowAll().flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId }
.flatMapLatest { groupId ->
appDb.bookDao.flowByGroup(groupId).map { list ->
sortBooks(list, groupId)
sortBooks(list, groupsFlow.value.find { it.groupId == groupId })
}
}.flowOn(Dispatchers.Default)
private val groupPreviewsFlow = combine(groupsFlow, allBooksFlow) { groups, allBooks ->
if (BookshelfConfig.bookGroupStyle in 2..3) {
groups.associate { group ->
val groupBooks = if (group.groupId == BookGroup.IdAll) {
allBooks.take(4)
} else {
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
}
group.groupId to groupBooks
}
} else {
emptyMap()
}
}.distinctUntilChanged().flowOn(Dispatchers.Default)
private val internalStateFlow = combine(
groupIdFlow,
searchKeyFlow,
@@ -131,8 +152,9 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val uiState: StateFlow<BookshelfUiState> = combine(
booksFlow,
groupsFlow,
groupPreviewsFlow,
internalStateFlow
) { books, groups, internal ->
) { books, groups, previews, internal ->
val filteredBooks = if (internal.searchKey.isEmpty()) {
books
} else {
@@ -143,28 +165,14 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
) || it.author.contains(internal.searchKey, true)
}
}
val currentGroups = groups ?: emptyList()
val isLandscape =
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
// 计算分组预览图
val previews = mutableMapOf<Long, List<Book>>()
if (BookshelfConfig.bookGroupStyle in 2..3) {
currentGroups.forEach { group ->
val groupBooks = if (group.groupId == BookGroup.IdAll) {
books.take(4)
} else {
books.filter { (it.group and group.groupId) != 0L }.take(4)
}
previews[group.groupId] = groupBooks
}
}
BookshelfUiState(
items = filteredBooks,
groups = currentGroups,
groups = groups,
groupPreviews = previews,
selectedGroupIndex = currentGroups.indexOfFirst { it.groupId == internal.groupId }
selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
.coerceAtLeast(0),
searchKey = internal.searchKey,
isSearch = internal.searchKey.isNotEmpty(),
@@ -203,8 +211,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
upTocPool.close()
}
private fun sortBooks(list: List<Book>, groupId: Long): List<Book> {
val group = appDb.bookGroupDao.getByID(groupId)
private fun sortBooks(list: List<Book>, group: BookGroup?): List<Book> {
val bookSort = group?.getRealBookSort() ?: AppConfig.bookshelfSort
val isDescending = AppConfig.bookshelfSortOrder == 1
@@ -238,8 +245,31 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getBooksFlow(groupId: Long): Flow<List<Book>> {
return combine(
appDb.bookDao.flowByGroup(groupId),
searchKeyFlow,
groupsFlow,
refreshTrigger
) { books, searchKey, groups, _ ->
val group = groups.find { it.groupId == groupId }
val filtered = if (searchKey.isEmpty()) {
books
} else {
books.filter {
it.name.contains(searchKey, true) || it.author.contains(searchKey, true)
}
}
sortBooks(filtered, group)
}.distinctUntilChanged().flowOn(Dispatchers.Default)
}
fun changeGroup(groupId: Long) {
groupIdFlow.value = groupId
if (groupIdFlow.value != groupId) {
groupIdFlow.value = groupId
BookshelfConfig.saveTabPosition = groupId
}
}
fun setSearchKey(key: String) {
@@ -174,8 +174,15 @@ fun Context.getPrefInt(key: String, defValue: Int = 0) =
fun Context.putPrefInt(key: String, value: Int) =
defaultSharedPreferences.edit { putInt(key, value) }
fun Context.getPrefLong(key: String, defValue: Long = 0L) =
defaultSharedPreferences.getLong(key, defValue)
fun Context.getPrefLong(key: String, defValue: Long = 0L): Long {
return try {
defaultSharedPreferences.getLong(key, defValue)
} catch (e: ClassCastException) {
val value = defaultSharedPreferences.getInt(key, defValue.toInt()).toLong()
defaultSharedPreferences.edit { putLong(key, value) }
value
}
}
fun Context.putPrefLong(key: String, value: Long) =
defaultSharedPreferences.edit { putLong(key, value) }