HapeLee 7 месяцев назад
Родитель
Сommit
c469cab5d6

+ 0 - 6
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) {

+ 6 - 0
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)
+
 }

+ 48 - 24
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<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,

+ 53 - 23
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<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) {

+ 9 - 2
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) }