[优化] 优化书架性能

This commit is contained in:
HapeLee committed 2026-03-17 00:50:59 +08:00
1 parent 8c46d2fd27
commit c469cab5d6
5 files changed
+116 -55

No files matched your search

@@ -276,12 +276,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefInt(PreferKey.bookshelfLayoutGridLandscape, value) appCtx.putPrefInt(PreferKey.bookshelfLayoutGridLandscape, value)
} }
var saveTabPosition: Int
get() = appCtx.getPrefInt(PreferKey.saveTabPosition, 0)
set(value) {
appCtx.putPrefInt(PreferKey.saveTabPosition, value)
}
var bookExportFileName: String? var bookExportFileName: String?
get() = appCtx.getPrefString(PreferKey.bookExportFileName) get() = appCtx.getPrefString(PreferKey.bookExportFileName)
set(value) { set(value) {
@@ -2,6 +2,7 @@ package io.legado.app.ui.config.bookshelfConfig
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.config.prefDelegate import io.legado.app.ui.config.prefDelegate
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
@@ -117,4 +118,9 @@ object BookshelfConfig {
*/ */
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false) 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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecondaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -50,7 +50,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow 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.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow 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.book.search.SearchActivity
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.file.HandleFileContract 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.cover.BookCoverWithProgress
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.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -131,11 +131,19 @@ fun BookshelfScreen(
val bookGroupStyle = BookshelfConfig.bookGroupStyle val bookGroupStyle = BookshelfConfig.bookGroupStyle
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) } var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
val title = if (bookGroupStyle == 2 && uiState.groups.isNotEmpty()) { val title = when {
if (isInFolderRoot) "书架" bookGroupStyle == 1 -> {
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: "书架" uiState.groups.getOrNull(pagerState.currentPage)?.groupName
} else { ?: 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) { if (bookGroupStyle == 2 && !isInFolderRoot) {
@@ -228,11 +236,12 @@ fun BookshelfScreen(
bottomContent = if (bookGroupStyle == 0) { bottomContent = if (bookGroupStyle == 0) {
{ {
if (uiState.groups.isNotEmpty()) { if (uiState.groups.isNotEmpty()) {
SecondaryScrollableTabRow( PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
edgePadding = 16.dp, edgePadding = 0.dp,
containerColor = Color.Transparent, divider = { },
divider = {} containerColor = GlassTopAppBarDefaults.containerColor(),
minTabWidth = 0.dp
) { ) {
uiState.groups.forEachIndexed { index, group -> uiState.groups.forEachIndexed { index, group ->
Tab( Tab(
@@ -240,7 +249,15 @@ fun BookshelfScreen(
onClick = { onClick = {
scope.launch { pagerState.animateScrollToPage(index) } 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 { } else {
HorizontalPager( HorizontalPager(
state = pagerState, state = pagerState,
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize(), beyondViewportPageCount = 1,
beyondViewportPageCount = 1 key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
) { ) { pageIndex ->
BookshelfPage( val group = uiState.groups.getOrNull(pageIndex)
paddingValues = paddingValues, if (group != null) {
uiState = uiState, val books by viewModel.getBooksFlow(group.groupId)
onBookClick = onBookClick, .collectAsState(emptyList())
onBookLongClick = onBookLongClick BookshelfPage(
) paddingValues = paddingValues,
books = books,
uiState = uiState,
onBookClick = onBookClick,
onBookLongClick = onBookLongClick
)
}
} }
} }
} }
@@ -419,6 +442,7 @@ fun BookshelfScreen(
@Composable @Composable
fun BookshelfPage( fun BookshelfPage(
paddingValues: PaddingValues, paddingValues: PaddingValues,
books: List<Book>,
uiState: BookshelfUiState, uiState: BookshelfUiState,
onBookClick: (Book) -> Unit, onBookClick: (Book) -> Unit,
onBookLongClick: (Book) -> Unit onBookLongClick: (Book) -> Unit
@@ -431,7 +455,7 @@ fun BookshelfPage(
bottom = 56.dp bottom = 56.dp
) )
) { ) {
items(uiState.items, key = { it.bookUrl }) { book -> items(books, key = { it.bookUrl }) { book ->
BookItem( BookItem(
book = book, book = book,
layoutMode = uiState.bookshelfLayoutMode, layoutMode = uiState.bookshelfLayoutMode,
@@ -455,7 +479,7 @@ fun BookshelfPage(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(uiState.items, key = { it.bookUrl }) { book -> items(books, key = { it.bookUrl }) { book ->
BookItem( BookItem(
book = book, book = book,
layoutMode = uiState.bookshelfLayoutMode, layoutMode = uiState.bookshelfLayoutMode,
@@ -54,6 +54,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@@ -61,6 +62,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -84,7 +86,7 @@ import kotlin.math.min
class BookshelfViewModel(application: Application) : BaseViewModel(application) { class BookshelfViewModel(application: Application) : BaseViewModel(application) {
var addBookJob: Coroutine<*>? = null var addBookJob: Coroutine<*>? = null
private val groupIdFlow = MutableStateFlow(BookGroup.IdAll) private val groupIdFlow = MutableStateFlow(BookshelfConfig.saveTabPosition)
private val searchKeyFlow = MutableStateFlow("") private val searchKeyFlow = MutableStateFlow("")
private val refreshTrigger = MutableStateFlow(0) private val refreshTrigger = MutableStateFlow(0)
private val loadingTextFlow = MutableStateFlow<String?>(null) private val loadingTextFlow = MutableStateFlow<String?>(null)
@@ -102,16 +104,35 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1) 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) @OptIn(ExperimentalCoroutinesApi::class)
val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId } val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId }
.flatMapLatest { groupId -> .flatMapLatest { groupId ->
appDb.bookDao.flowByGroup(groupId).map { list -> appDb.bookDao.flowByGroup(groupId).map { list ->
sortBooks(list, groupId) sortBooks(list, groupsFlow.value.find { it.groupId == groupId })
} }
}.flowOn(Dispatchers.Default) }.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( private val internalStateFlow = combine(
groupIdFlow, groupIdFlow,
searchKeyFlow, searchKeyFlow,
@@ -131,8 +152,9 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
val uiState: StateFlow<BookshelfUiState> = combine( val uiState: StateFlow<BookshelfUiState> = combine(
booksFlow, booksFlow,
groupsFlow, groupsFlow,
groupPreviewsFlow,
internalStateFlow internalStateFlow
) { books, groups, internal -> ) { books, groups, previews, internal ->
val filteredBooks = if (internal.searchKey.isEmpty()) { val filteredBooks = if (internal.searchKey.isEmpty()) {
books books
} else { } else {
@@ -143,28 +165,14 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
) || it.author.contains(internal.searchKey, true) ) || it.author.contains(internal.searchKey, true)
} }
} }
val currentGroups = groups ?: emptyList()
val isLandscape = val isLandscape =
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE 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( BookshelfUiState(
items = filteredBooks, items = filteredBooks,
groups = currentGroups, groups = groups,
groupPreviews = previews, groupPreviews = previews,
selectedGroupIndex = currentGroups.indexOfFirst { it.groupId == internal.groupId } selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
.coerceAtLeast(0), .coerceAtLeast(0),
searchKey = internal.searchKey, searchKey = internal.searchKey,
isSearch = internal.searchKey.isNotEmpty(), isSearch = internal.searchKey.isNotEmpty(),
@@ -203,8 +211,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
upTocPool.close() upTocPool.close()
} }
private fun sortBooks(list: List<Book>, groupId: Long): List<Book> { private fun sortBooks(list: List<Book>, group: BookGroup?): List<Book> {
val group = appDb.bookGroupDao.getByID(groupId)
val bookSort = group?.getRealBookSort() ?: AppConfig.bookshelfSort val bookSort = group?.getRealBookSort() ?: AppConfig.bookshelfSort
val isDescending = AppConfig.bookshelfSortOrder == 1 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) { fun changeGroup(groupId: Long) {
groupIdFlow.value = groupId if (groupIdFlow.value != groupId) {
groupIdFlow.value = groupId
BookshelfConfig.saveTabPosition = groupId
}
} }
fun setSearchKey(key: String) { fun setSearchKey(key: String) {
@@ -174,8 +174,15 @@ fun Context.getPrefInt(key: String, defValue: Int = 0) =
fun Context.putPrefInt(key: String, value: Int) = fun Context.putPrefInt(key: String, value: Int) =
defaultSharedPreferences.edit { putInt(key, value) } defaultSharedPreferences.edit { putInt(key, value) }
fun Context.getPrefLong(key: String, defValue: Long = 0L) = fun Context.getPrefLong(key: String, defValue: Long = 0L): Long {
defaultSharedPreferences.getLong(key, defValue) 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) = fun Context.putPrefLong(key: String, value: Long) =
defaultSharedPreferences.edit { putLong(key, value) } defaultSharedPreferences.edit { putLong(key, value) }