diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index f360721c0..7ba759efd 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt index 9a54384bd..7f9ef694b 100644 --- a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt @@ -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) + } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index f1a77b13c..f24eb3011 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -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, 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, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index f010f5d8e..1f3c9eb20 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -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(null) @@ -102,16 +104,35 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) val scrollTrigger = MutableSharedFlow(extraBufferCapacity = 1) - val groupsFlow = appDb.bookGroupDao.show.asFlow() + val groupsFlow: StateFlow> = 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 = 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>() - 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, groupId: Long): List { - val group = appDb.bookGroupDao.getByID(groupId) + private fun sortBooks(list: List, group: BookGroup?): List { 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> { + 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) { diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index b44410399..ee4ccfacc 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -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) }