[优化] 优化书架性能
This commit is contained in:
@@ -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) }
|
||||
|
||||
Reference in New Issue
Block a user