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

+ 26 - 0
app/src/main/java/io/legado/app/data/dao/BookDao.kt

@@ -12,6 +12,7 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookSource
 import io.legado.app.help.book.isNotShelf
+import io.legado.app.ui.main.bookshelf.BookShelfItem
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
@@ -51,6 +52,31 @@ interface BookDao {
     @Query("SELECT * FROM books order by durChapterTime desc")
     fun flowAll(): Flow<List<Book>>
 
+    @Query(
+        """
+    SELECT 
+        bookUrl,
+        name,
+        author,
+        coverUrl,
+        customCoverUrl,
+        durChapterTitle,
+        durChapterTime,
+        durChapterPos,
+        latestChapterTitle,
+        latestChapterTime,
+        totalChapterNum,
+        durChapterIndex,
+        type,
+        `group`,
+        `order`,
+        canUpdate
+    FROM books
+    ORDER BY durChapterTime DESC
+"""
+    )
+    fun flowBookShelf(): Flow<List<BookShelfItem>>
+
     @Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0")
     fun flowAudio(): Flow<List<Book>>
 

+ 5 - 7
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -32,9 +32,7 @@ import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookGroup
-import io.legado.app.help.book.isLocal
 import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.widget.components.cover.BookCover
 import io.legado.app.ui.widget.components.cover.BookCoverWithProgress
@@ -197,7 +195,7 @@ fun BookshelfItem(
 
 @Composable
 fun BookGroupCover(
-    books: List<Book>,
+    books: List<BookShelfItem>,
     coverPath: String? = null,
     modifier: Modifier = Modifier
 ) {
@@ -289,7 +287,7 @@ fun BookGroupCover(
 @Composable
 fun BookGroupItemGrid(
     group: BookGroup,
-    previewBooks: List<Book>,
+    previewBooks: List<BookShelfItem>,
     gridStyle: Int = 0,
     titleSmallFont: Boolean = false,
     titleCenter: Boolean = true,
@@ -318,7 +316,7 @@ fun BookGroupItemGrid(
 @Composable
 fun BookGroupItemList(
     group: BookGroup,
-    previewBooks: List<Book>,
+    previewBooks: List<BookShelfItem>,
     isCompact: Boolean = false,
     titleSmallFont: Boolean = false,
     titleCenter: Boolean = true,
@@ -348,7 +346,7 @@ fun BookGroupItemList(
 
 @Composable
 fun BookItem(
-    book: Book,
+    book: BookShelfItem,
     layoutMode: Int,
     gridStyle: Int = 0,
     isCompact: Boolean = false,
@@ -374,7 +372,7 @@ fun BookItem(
                 isUpdating = isUpdating,
                 modifier = modifier,
                 badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
-                showBadgeDot = !BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && unreadCount > 0 && book.lastCheckCount > 0
+                showBadgeDot = !BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && unreadCount > 0
             )
         },
         title = book.name,

+ 55 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt

@@ -0,0 +1,55 @@
+package io.legado.app.ui.main.bookshelf
+
+import io.legado.app.constant.BookType
+import io.legado.app.data.entities.Book
+import kotlin.math.max
+
+data class BookShelfItem(
+    val bookUrl: String,
+    val name: String,
+    val author: String,
+    val coverUrl: String?,
+    val customCoverUrl: String?,
+    val durChapterTitle: String?,
+    val durChapterTime: Long,
+    val durChapterPos: Int,
+    val latestChapterTitle: String?,
+    val latestChapterTime: Long,
+    val totalChapterNum: Int,
+    val durChapterIndex: Int,
+    val type: Int,
+    val group: Long,
+    val order: Int,
+    val canUpdate: Boolean = true
+) {
+    fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
+
+    val isLocal: Boolean get() = (type and BookType.local) > 0
+
+    val isAudio: Boolean get() = (type and BookType.audio) > 0
+
+    val isImage: Boolean get() = (type and BookType.image) > 0
+
+    fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
+}
+
+fun Book.toBookShelfItem(): BookShelfItem {
+    return BookShelfItem(
+        bookUrl = bookUrl,
+        name = name,
+        author = author,
+        coverUrl = coverUrl,
+        customCoverUrl = customCoverUrl,
+        durChapterTitle = durChapterTitle,
+        durChapterTime = durChapterTime,
+        durChapterPos = durChapterPos,
+        latestChapterTitle = latestChapterTitle,
+        latestChapterTime = latestChapterTime,
+        totalChapterNum = totalChapterNum,
+        durChapterIndex = durChapterIndex,
+        type = type,
+        group = group,
+        order = order,
+        canUpdate = canUpdate
+    )
+}

+ 74 - 27
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -11,14 +11,19 @@ import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.grid.itemsIndexed
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
+import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.FileOpen
@@ -65,7 +70,6 @@ import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Dialog
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
-import io.legado.app.data.entities.Book
 import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.book.cache.CacheActivity
 import io.legado.app.ui.book.import.local.ImportBookActivity
@@ -74,10 +78,12 @@ 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.widget.components.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
 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.lazylist.FastScrollLazyVerticalGrid
 import io.legado.app.ui.widget.components.list.ListScaffold
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.utils.readText
 import io.legado.app.utils.startActivity
@@ -94,8 +100,8 @@ import org.koin.androidx.compose.koinViewModel
 @Composable
 fun BookshelfScreen(
     viewModel: BookshelfViewModel = koinViewModel(),
-    onBookClick: (Book) -> Unit,
-    onBookLongClick: (Book) -> Unit
+    onBookClick: (BookShelfItem) -> Unit,
+    onBookLongClick: (BookShelfItem) -> Unit
 ) {
     val context = LocalContext.current
     val uiState by viewModel.uiState.collectAsState()
@@ -107,6 +113,7 @@ fun BookshelfScreen(
     var showConfigSheet by remember { mutableStateOf(false) }
     var showGroupManageSheet by remember { mutableStateOf(false) }
     var showLogSheet by remember { mutableStateOf(false) }
+    var showGroupMenu by remember { mutableStateOf(false) }
 
     val clipboardManager = LocalClipboard.current
     val snackbarHostState = remember { SnackbarHostState() }
@@ -309,29 +316,69 @@ fun BookshelfScreen(
                 if (uiState.groups.isNotEmpty()) {
                     val selectedTabIndex =
                         pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
-                    PrimaryScrollableTabRow(
-                        selectedTabIndex = selectedTabIndex,
-                        edgePadding = 0.dp,
-                        divider = { },
-                        containerColor = GlassTopAppBarDefaults.containerColor(),
-                        minTabWidth = 0.dp
+                    Row(
+                        modifier = Modifier.fillMaxWidth(),
+                        verticalAlignment = Alignment.CenterVertically
                     ) {
-                        uiState.groups.forEachIndexed { index, group ->
-                            Tab(
-                                selected = selectedTabIndex == index,
-                                onClick = {
-                                    scope.launch { pagerState.animateScrollToPage(index) }
-                                },
-                                text = {
-                                    Text(
-                                        text = group.groupName,
-                                        maxLines = 1,
-                                        overflow = TextOverflow.Ellipsis,
-                                        modifier = Modifier.padding(horizontal = 8.dp),
-                                        color = MaterialTheme.colorScheme.onSurfaceVariant
-                                    )
+                        PrimaryScrollableTabRow(
+                            selectedTabIndex = selectedTabIndex,
+                            edgePadding = 0.dp,
+                            divider = { },
+                            containerColor = GlassTopAppBarDefaults.containerColor(),
+                            minTabWidth = 0.dp,
+                            modifier = Modifier.weight(1f)
+                        ) {
+                            uiState.groups.forEachIndexed { index, group ->
+                                Tab(
+                                    selected = selectedTabIndex == index,
+                                    onClick = {
+                                        scope.launch { pagerState.animateScrollToPage(index) }
+                                    },
+                                    text = {
+                                        Text(
+                                            text = group.groupName,
+                                            maxLines = 1,
+                                            overflow = TextOverflow.Ellipsis,
+                                            modifier = Modifier.padding(horizontal = 8.dp),
+                                            color = MaterialTheme.colorScheme.onSurfaceVariant
+                                        )
+                                    }
+                                )
+                            }
+                        }
+
+                        if (BookshelfConfig.shouldShowExpandButton) {
+                            Box(modifier = Modifier.padding(end = 16.dp)) {
+                                SmallOutlinedIconToggleButton(
+                                    checked = showGroupMenu,
+                                    onCheckedChange = { showGroupMenu = it },
+                                    icon = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                    contentDescription = stringResource(R.string.group_manage)
+                                )
+                                RoundDropdownMenu(
+                                    expanded = showGroupMenu,
+                                    onDismissRequest = { showGroupMenu = false }
+                                ) { dismiss ->
+                                    uiState.groups.forEachIndexed { index, group ->
+                                        RoundDropdownMenuItem(
+                                            text = { Text(group.groupName) },
+                                            onClick = {
+                                                scope.launch { pagerState.animateScrollToPage(index) }
+                                                dismiss()
+                                            },
+                                            trailingIcon = {
+                                                if (selectedTabIndex == index) {
+                                                    Icon(
+                                                        Icons.Default.Check,
+                                                        null,
+                                                        modifier = Modifier.size(18.dp)
+                                                    )
+                                                }
+                                            }
+                                        )
+                                    }
                                 }
-                            )
+                            }
                         }
                     }
                 }
@@ -533,12 +580,12 @@ fun BookshelfScreen(
 @Composable
 fun BookshelfPage(
     paddingValues: PaddingValues,
-    books: List<Book>,
+    books: List<BookShelfItem>,
     uiState: BookshelfUiState,
     bookshelfLayoutMode: Int,
     bookshelfLayoutGrid: Int,
-    onBookClick: (Book) -> Unit,
-    onBookLongClick: (Book) -> Unit
+    onBookClick: (BookShelfItem) -> Unit,
+    onBookLongClick: (BookShelfItem) -> Unit
 ) {
     FastScrollLazyVerticalGrid(
         columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),

+ 3 - 4
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt

@@ -1,18 +1,17 @@
 package io.legado.app.ui.main.bookshelf
 
-import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.ui.widget.components.list.ListUiState
 
 data class BookshelfUiState(
-    override val items: List<Book> = emptyList(),
+    override val items: List<BookShelfItem> = emptyList(),
     override val selectedIds: Set<Any> = emptySet(),
     override val searchKey: String = "",
     override val isSearch: Boolean = false,
     override val isLoading: Boolean = false,
     val groups: List<BookGroup> = emptyList(),
-    val groupPreviews: Map<Long, List<Book>> = emptyMap(),
+    val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
     val selectedGroupIndex: Int = 0,
     val loadingText: String? = null,
     val updatingBooks: Set<String> = emptySet()
-) : ListUiState<Book>
+) : ListUiState<BookShelfItem>

+ 28 - 23
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -22,7 +22,6 @@ import io.legado.app.data.repository.UploadRepository
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.addType
-import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isUpError
 import io.legado.app.help.book.removeType
 import io.legado.app.help.book.sync
@@ -123,13 +122,15 @@ class BookshelfViewModel(
     val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
 
-    private val allBooksFlow = appDb.bookDao.flowAll().flowOn(Dispatchers.Default)
+    private val allBooksFlow = appDb.bookDao.flowBookShelf().flowOn(Dispatchers.Default)
 
     @OptIn(ExperimentalCoroutinesApi::class)
     val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId }
         .flatMapLatest { groupId ->
             appDb.bookDao.flowByGroup(groupId).map { list ->
-                sortBooks(list, groupsFlow.value.find { it.groupId == groupId })
+                sortBooks(
+                    list.map { it.toBookShelfItem() },
+                    groupsFlow.value.find { it.groupId == groupId })
             }
         }.flowOn(Dispatchers.Default)
 
@@ -269,7 +270,7 @@ class BookshelfViewModel(
         upTocPool.close()
     }
 
-    private fun sortBooks(list: List<Book>, group: BookGroup?): List<Book> {
+    private fun sortBooks(list: List<BookShelfItem>, group: BookGroup?): List<BookShelfItem> {
         val bookSort = group?.getRealBookSort() ?: BookshelfConfig.bookshelfSort
         val isDescending = BookshelfConfig.bookshelfSortOrder == 1
 
@@ -304,7 +305,7 @@ class BookshelfViewModel(
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    fun getBooksFlow(groupId: Long): Flow<List<Book>> {
+    fun getBooksFlow(groupId: Long): Flow<List<BookShelfItem>> {
         return combine(
             appDb.bookDao.flowByGroup(groupId),
             searchKeyFlow,
@@ -312,10 +313,11 @@ class BookshelfViewModel(
             refreshTrigger
         ) { books, searchKey, groups, _ ->
             val group = groups.find { it.groupId == groupId }
+            val bookshelfItems = books.map { it.toBookShelfItem() }
             val filtered = if (searchKey.isEmpty()) {
-                books
+                bookshelfItems
             } else {
-                books.filter {
+                bookshelfItems.filter {
                     it.name.contains(searchKey, true) || it.author.contains(searchKey, true)
                 }
             }
@@ -349,13 +351,11 @@ class BookshelfViewModel(
         }
     }
 
-    fun upToc(books: List<Book>) {
+    fun upToc(books: List<BookShelfItem>) {
         execute(context = upTocPool) {
-            books.filter {
-                !it.isLocal && it.canUpdate
-            }.let {
-                addToWaitUp(it)
-            }
+            val bookUrls = books.filter { !it.isLocal && it.canUpdate }.map { it.bookUrl }
+            val fullBooks = bookUrls.mapNotNull { appDb.bookDao.getBook(it) }
+            addToWaitUp(fullBooks)
         }
     }
 
@@ -562,17 +562,20 @@ class BookshelfViewModel(
         }
     }
 
-    fun exportToUri(uri: Uri, books: List<Book>) {
+    fun exportToUri(uri: Uri, items: List<BookShelfItem>) {
         execute {
             context.contentResolver.openOutputStream(uri)?.use { out ->
                 val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
                 writer.setIndent("  ")
                 writer.beginArray()
-                books.forEach {
+                items.forEach {
                     val bookMap = hashMapOf<String, String?>()
                     bookMap["name"] = it.name
                     bookMap["author"] = it.author
-                    bookMap["intro"] = it.getDisplayIntro()
+                    // intro is not in BookShelfItem, fetch from DB if needed or skip
+                    // For now, let's keep it simple and skip intro or fetch it
+                    val fullBook = appDb.bookDao.getBook(it.bookUrl)
+                    bookMap["intro"] = fullBook?.getDisplayIntro()
                     GSON.toJson(bookMap, bookMap::class.java, writer)
                 }
                 writer.endArray()
@@ -585,14 +588,15 @@ class BookshelfViewModel(
         }
     }
 
-    fun uploadBookshelf(books: List<Book>) {
+    fun uploadBookshelf(items: List<BookShelfItem>) {
         execute {
             val json = withContext(Dispatchers.Default) {
-                val list = books.map {
+                val list = items.map {
                     val bookMap = hashMapOf<String, String?>()
                     bookMap["name"] = it.name
                     bookMap["author"] = it.author
-                    bookMap["intro"] = it.getDisplayIntro()
+                    val fullBook = appDb.bookDao.getBook(it.bookUrl)
+                    bookMap["intro"] = fullBook?.getDisplayIntro()
                     bookMap
                 }
                 GSON.toJson(list)
@@ -619,9 +623,9 @@ class BookshelfViewModel(
         }
     }
 
-    fun exportBookshelf(books: List<Book>?, success: (file: File) -> Unit) {
+    fun exportBookshelf(items: List<BookShelfItem>?, success: (file: File) -> Unit) {
         execute {
-            books?.let {
+            items?.let {
                 val path = "${context.filesDir}/books.json"
                 FileUtils.delete(path)
                 val file = FileUtils.createFileWithReplace(path)
@@ -629,11 +633,12 @@ class BookshelfViewModel(
                     val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
                     writer.setIndent("  ")
                     writer.beginArray()
-                    books.forEach {
+                    items.forEach {
                         val bookMap = hashMapOf<String, String?>()
                         bookMap["name"] = it.name
                         bookMap["author"] = it.author
-                        bookMap["intro"] = it.getDisplayIntro()
+                        val fullBook = appDb.bookDao.getBook(it.bookUrl)
+                        bookMap["intro"] = fullBook?.getDisplayIntro()
                         GSON.toJson(bookMap, bookMap::class.java, writer)
                     }
                     writer.endArray()

+ 17 - 1
app/src/main/java/io/legado/app/utils/ContextExtensions.kt

@@ -50,6 +50,7 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.ui.book.audio.AudioPlayActivity
 import io.legado.app.ui.book.manga.ReadMangaActivity
 import io.legado.app.ui.book.read.ReadBookActivity
+import io.legado.app.ui.main.bookshelf.BookShelfItem
 import splitties.systemservices.clipboardManager
 import splitties.systemservices.connectivityManager
 import splitties.systemservices.uiModeManager
@@ -80,6 +81,22 @@ fun Context.startActivityForBook(
     startActivity(intent)
 }
 
+fun Context.startActivityForBook(
+    book: BookShelfItem,
+    configIntent: Intent.() -> Unit = {},
+) {
+    val cls = when {
+        book.isAudio -> AudioPlayActivity::class.java
+        !book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
+        else -> ReadBookActivity::class.java
+    }
+    val intent = Intent(this, cls)
+    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+    intent.putExtra("bookUrl", book.bookUrl)
+    intent.apply(configIntent)
+    startActivity(intent)
+}
+
 inline fun <reified T : Service> Context.startService(configIntent: Intent.() -> Unit = {}) {
     startService(Intent(this, T::class.java).apply(configIntent))
 }
@@ -464,4 +481,3 @@ fun Context.themeColor(attr: Int): Int {
         typedValue.data
     }
 }
-