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