HapeLee 5 miesięcy temu
rodzic
commit
207b00189f

+ 41 - 27
app/src/main/java/io/legado/app/data/dao/BookDao.kt

@@ -98,7 +98,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         where type & ${BookType.text} > 0
         and type & ${BookType.local} = 0
@@ -134,7 +135,8 @@ interface BookDao {
         `order`,
         canUpdate,
         ifnull(customIntro, intro) as intro,
-        kind
+        kind,
+        wordCount
     FROM books
     ORDER BY durChapterTime DESC
 """
@@ -167,7 +169,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books
         WHERE type & ${BookType.audio} > 0
         """
@@ -200,7 +203,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE type & ${BookType.local} > 0
         """
@@ -238,7 +242,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
         and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
@@ -277,7 +282,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         where type & ${BookType.local} > 0
         and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
@@ -311,7 +317,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE (`group` & :group) > 0
         """
@@ -346,7 +353,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
         """
@@ -379,7 +387,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         where type & ${BookType.updateError} > 0 
         order by durChapterTime desc
@@ -413,7 +422,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE durChapterIndex = 0 AND durChapterPos = 0
         """
@@ -446,7 +456,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
         """
@@ -479,7 +490,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
         """
@@ -512,7 +524,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE type & ${BookType.image} > 0
         """
@@ -545,7 +558,8 @@ interface BookDao {
             `order`,
             canUpdate,
             ifnull(customIntro, intro) as intro,
-            kind
+            kind,
+            wordCount
         FROM books 
         WHERE type & ${BookType.text} > 0
         """
@@ -721,7 +735,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         ORDER BY durChapterTime DESC
         LIMIT 10
@@ -736,7 +750,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.text} > 0 AND type & ${BookType.local} = 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
@@ -754,7 +768,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.local} > 0
         ORDER BY durChapterTime DESC
@@ -770,7 +784,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.audio} > 0
         ORDER BY durChapterTime DESC
@@ -786,7 +800,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.audio} = 0 AND type & ${BookType.local} = 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
@@ -803,7 +817,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.local} > 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
@@ -820,7 +834,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.image} > 0
         ORDER BY durChapterTime DESC
@@ -836,7 +850,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.text} > 0
         ORDER BY durChapterTime DESC
@@ -852,7 +866,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE type & ${BookType.updateError} > 0
         ORDER BY durChapterTime DESC
@@ -868,7 +882,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE durChapterIndex = 0 AND durChapterPos = 0
         ORDER BY durChapterTime DESC
@@ -884,7 +898,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
         ORDER BY durChapterTime DESC
@@ -900,7 +914,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
         ORDER BY durChapterTime DESC
@@ -916,7 +930,7 @@ interface BookDao {
             durChapterPos, latestChapterTitle, latestChapterTime,
             lastCheckCount, totalChapterNum, durChapterIndex,
             type, `group`, `order`, canUpdate,
-            ifnull(customIntro, intro) as intro, kind
+            ifnull(customIntro, intro) as intro, kind, wordCount
         FROM books
         WHERE (`group` & :groupId) > 0
         ORDER BY durChapterTime DESC

+ 3 - 0
app/src/main/java/io/legado/app/data/entities/Book.kt

@@ -355,6 +355,9 @@ data class Book(
             newBook.type = type
         }
         newBook.readConfig = readConfig
+        if (newBook.wordCount.isNullOrBlank()) {
+            newBook.wordCount = wordCount
+        }
         return newBook
     }
 

+ 44 - 1
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -2,7 +2,6 @@
 
 package io.legado.app.help.book
 
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import androidx.core.net.toUri
 import com.script.buildScriptBindings
@@ -19,6 +18,7 @@ import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.RuleBigDataHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.model.localBook.LocalBook
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.GSON
 import io.legado.app.utils.MD5Utils
@@ -27,6 +27,7 @@ import io.legado.app.utils.find
 import io.legado.app.utils.inputStream
 import io.legado.app.utils.isUri
 import io.legado.app.utils.normalizeFileName
+import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.toastOnUi
 import splitties.init.appCtx
 import java.io.File
@@ -306,6 +307,44 @@ fun Book.upType() {
     }
 }
 
+fun Book.upKind() {
+    val fileSizePattern = Regex("""^\d[\d,.]*\s*(b|kb|M|G|T)$""", RegexOption.IGNORE_CASE)
+    val wordCountPattern = Regex(""".*?\d.*字$""")
+    val kinds = kind?.splitNotBlank(",", "\n").orEmpty()
+        .filter {
+            it.isNotBlank() && !fileSizePattern.matches(it.trim()) && !wordCountPattern.matches(
+                it.trim()
+            )
+        }
+        .toMutableList()
+
+    if (isLocal) {
+        // 添加格式
+        val typeName = getBookTypeName()
+        if (typeName != "未知类型" && !kinds.contains(typeName)) {
+            kinds.add(0, typeName)
+        }
+        // 添加大小
+        try {
+            val size = FileDoc.fromFile(bookUrl).size
+            if (size > 0) {
+                kinds.add(io.legado.app.utils.ConvertUtils.formatFileSize(size))
+            }
+        } catch (e: Exception) {
+            // ignore
+        }
+    }
+
+    // 添加字数
+    wordCount?.let {
+        if (it.isNotBlank()) {
+            kinds.add(it)
+        }
+    }
+
+    kind = kinds.distinct().joinToString(",")
+}
+
 fun Book.sync(oldBook: Book) {
     val curBook = appDb.bookDao.getBook(oldBook.bookUrl)!!
     durChapterTime = curBook.durChapterTime
@@ -344,6 +383,10 @@ fun Book.updateTo(newBook: Book): Book {
         newBook.type = type
     }
     newBook.readConfig = readConfig
+    if (newBook.wordCount.isNullOrBlank()) {
+        newBook.wordCount = wordCount
+    }
+    newBook.upKind()
     val variableMap = variableMap.toMutableMap()
     variableMap.keys.removeIf {
         newBook.hasVariable(it)

+ 6 - 1
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -1,6 +1,5 @@
 package io.legado.app.model.localBook
 
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import android.util.Base64
 import androidx.core.net.toUri
@@ -36,11 +35,13 @@ import io.legado.app.help.book.isPdf
 import io.legado.app.help.book.isUmd
 import io.legado.app.help.book.removeLocalUriCache
 import io.legado.app.help.book.simulatedTotalChapterNum
+import io.legado.app.help.book.upKind
 import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.CustomUrl
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
@@ -252,14 +253,17 @@ object LocalBook {
                 order = appDb.bookDao.minOrder - 1
             )
             upBookInfo(book)
+            book.upKind()
             appDb.bookDao.insert(book)
         } else {
             deleteBook(book, false)
             upBookInfo(book)
+            book.upKind()
             // 触发 isLocalModified
             book.latestChapterTime = 0
             //已有书籍说明是更新,删除原有目录
             appDb.bookChapterDao.delByBook(bookUrl)
+            appDb.bookDao.update(book)
         }
         return book
     }
@@ -290,6 +294,7 @@ object LocalBook {
                     //附加压缩包名称 以便解压文件被删后再解压
                     origin = "${BookType.localTag}::${archiveFileDoc.name}"
                     addType(BookType.archive)
+                    upKind()
                     save()
                 }
             }

+ 2 - 0
app/src/main/java/io/legado/app/model/localBook/TextFile.kt

@@ -8,6 +8,7 @@ import io.legado.app.data.entities.TxtTocRule
 import io.legado.app.exception.EmptyFileException
 import io.legado.app.help.DefaultData
 import io.legado.app.help.book.isLocalModified
+import io.legado.app.help.book.upKind
 import io.legado.app.utils.EncodingDetect
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.StringUtils
@@ -94,6 +95,7 @@ class TextFile(private var book: Book) {
         }
         val (toc, wordCount) = analyze(book.tocUrl.toPattern(Pattern.MULTILINE))
         book.wordCount = StringUtils.wordCountFormat(wordCount)
+        book.upKind()
         toc.forEachIndexed { index, bookChapter ->
             bookChapter.index = index
             bookChapter.bookUrl = book.bookUrl

+ 3 - 14
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -36,6 +36,7 @@ import io.legado.app.help.book.isNotShelf
 import io.legado.app.help.book.isSameNameAuthor
 import io.legado.app.help.book.isWebFile
 import io.legado.app.help.book.removeType
+import io.legado.app.help.book.upKind
 import io.legado.app.help.book.updateTo
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.coroutine.Coroutine
@@ -51,8 +52,6 @@ import io.legado.app.model.webBook.WebBook
 import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
 import io.legado.app.utils.ArchiveUtils
-import io.legado.app.utils.ConvertUtils
-import io.legado.app.utils.FileDoc
 import io.legado.app.utils.GSON
 import io.legado.app.utils.ImageSaveUtils
 import io.legado.app.utils.UrlUtil
@@ -759,24 +758,14 @@ class BookInfoViewModel(
 
     private fun refreshMeta(book: Book) {
         execute {
-            val fileSizePattern = Regex("""^\d[\d,.]*\s*(b|kb|M|G|T)$""", RegexOption.IGNORE_CASE)
-            val kinds = (book.kind?.splitNotBlank(",", "\n").orEmpty().toList())
-                .filter { !fileSizePattern.matches(it.trim()) }
-                .toMutableList()
-            if (book.isLocal) {
-                val size = FileDoc.fromFile(book.bookUrl).size
-                if (size > 0) {
-                    kinds.add(ConvertUtils.formatFileSize(size))
-                }
-            }
-            val finalKinds = kinds.distinct()
+            book.upKind()
             val userGroupIds = appDb.bookGroupDao.idsSum
             val groupAnd = userGroupIds and book.group
             val hasCustomGroup = book.group > 0L && groupAnd != 0L
             val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
             val normalizedGroupNames = groupNames.ifBlank { null }
-            book.kind = finalKinds.joinToString(",")
             appDb.bookDao.update(book)
+            val finalKinds = book.kind?.splitNotBlank(",", "\n").orEmpty().toList()
             Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
         }.onSuccess {
             currentKindLabels = it.first

+ 15 - 14
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -54,7 +54,6 @@ import io.legado.app.ui.widget.components.cover.BookshelfCover
 import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.text.AppText
-import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.toTimeAgo
 
 /**
@@ -289,7 +288,7 @@ fun BookshelfItem(
 
 @Composable
 fun BookGroupCover(
-    books: List<BookShelfItem>,
+    books: List<BookUiItem>,
     coverPath: String? = null,
     leftBottomText: String? = null,
     modifier: Modifier = Modifier
@@ -328,7 +327,7 @@ fun BookGroupCover(
                                 .fillMaxHeight()
                                 .padding(1.dp)
                         ) {
-                            books.getOrNull(0)?.let {
+                            books.getOrNull(0)?.book?.let {
                                 CoilBookCover(
                                     name = it.name,
                                     author = it.author,
@@ -343,7 +342,7 @@ fun BookGroupCover(
                                 .fillMaxHeight()
                                 .padding(1.dp)
                         ) {
-                            books.getOrNull(1)?.let {
+                            books.getOrNull(1)?.book?.let {
                                 CoilBookCover(
                                     name = it.name,
                                     author = it.author,
@@ -360,7 +359,7 @@ fun BookGroupCover(
                                 .fillMaxHeight()
                                 .padding(1.dp)
                         ) {
-                            books.getOrNull(2)?.let {
+                            books.getOrNull(2)?.book?.let {
                                 CoilBookCover(
                                     name = it.name,
                                     author = it.author,
@@ -375,7 +374,7 @@ fun BookGroupCover(
                                 .fillMaxHeight()
                                 .padding(1.dp)
                         ) {
-                            books.getOrNull(3)?.let {
+                            books.getOrNull(3)?.book?.let {
                                 CoilBookCover(
                                     name = it.name,
                                     author = it.author,
@@ -406,7 +405,7 @@ fun BookGroupCover(
 @Composable
 fun BookGroupItemGrid(
     group: BookGroupUi,
-    previewBooks: List<BookShelfItem>,
+    previewBooks: List<BookUiItem>,
     countText: String? = null,
     gridStyle: Int = 0,
     titleSmallFont: Boolean = false,
@@ -443,7 +442,7 @@ fun BookGroupItemGrid(
 @Composable
 fun BookGroupItemList(
     group: BookGroupUi,
-    previewBooks: List<BookShelfItem>,
+    previewBooks: List<BookUiItem>,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     countText: String? = null,
@@ -467,7 +466,7 @@ fun BookGroupItemList(
         )
         return
     }
-    val firstBookName = previewBooks.firstOrNull()?.name
+    val firstBookName = previewBooks.firstOrNull()?.book?.name
     val descAnnotated = if (firstBookName != null) {
         buildAnnotatedString {
             append("最近阅读:")
@@ -499,7 +498,7 @@ fun BookGroupItemList(
 @Composable
 fun BookGroupItemHorizontalCovers(
     group: BookGroupUi,
-    previewBooks: List<BookShelfItem>,
+    previewBooks: List<BookUiItem>,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     countText: String? = null,
@@ -563,7 +562,8 @@ fun BookGroupItemHorizontalCovers(
                     horizontalArrangement = Arrangement.spacedBy(8.dp)
                 ) {
                     val coverCount = BookshelfConfig.bookshelfGroupCoverCount
-                    previewBooks.take(coverCount).forEach { book ->
+                    previewBooks.take(coverCount).forEach { bookUi ->
+                        val book = bookUi.book
                         Box(
                             modifier = Modifier
                                 .weight(1f)
@@ -597,7 +597,7 @@ fun BookGroupItemHorizontalCovers(
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun BookItem(
-    book: BookShelfItem,
+    bookUi: BookUiItem,
     layoutMode: Int,
     modifier: Modifier = Modifier,
     isSelected: Boolean = false,
@@ -616,6 +616,7 @@ fun BookItem(
     onClick: () -> Unit,
     onLongClick: (() -> Unit)?
 ) {
+    val book = bookUi.book
     val unreadCount = book.getUnreadChapterNum()
     val unreadText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null
     val bookTypeLabel = if (BookshelfConfig.showTip) {
@@ -684,10 +685,10 @@ fun BookItem(
         desc = book.durChapterTitle ?: "",
         columnContent = if (layoutMode == 0 && !isCompact && BookshelfConfig.showBookIntro) {
             {
-                val kindList = book.kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }
+                val kindList = bookUi.displayTags
                 val intro = book.intro?.takeIf { it.isNotBlank() }
                 val customTagColors = if (ThemeConfig.enableCustomTagColors) ThemeConfig.getCustomTagColors() else emptyList()
-                if (BookshelfConfig.bookshelfShowTag && !kindList.isNullOrEmpty()) {
+                if (BookshelfConfig.bookshelfShowTag && kindList.isNotEmpty()) {
                     Row(
                         modifier = Modifier
                             .fillMaxWidth()

+ 42 - 3
app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt

@@ -3,6 +3,9 @@ package io.legado.app.ui.main.bookshelf
 import androidx.compose.runtime.Stable
 import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
+import io.legado.app.utils.splitNotBlank
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
 import kotlin.math.max
 
 @Stable
@@ -27,7 +30,8 @@ data class BookShelfItem(
     val order: Int,
     val canUpdate: Boolean = true,
     val intro: String? = null,
-    val kind: String? = null
+    val kind: String? = null,
+    val wordCount: String? = null
 ) {
     fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
 
@@ -41,8 +45,41 @@ data class BookShelfItem(
 
     val isNew: Boolean get() = lastCheckCount > 0
 
-
     fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
+
+    /**
+     * 将 DTO 转换为专为 Compose 设计的 UI 状态
+     */
+    fun toUiItem(): BookUiItem {
+        val tagList = mutableListOf<String>()
+        kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }?.let {
+            tagList.addAll(it)
+        }
+        if (!wordCount.isNullOrBlank() && !tagList.contains(wordCount)) {
+            tagList.add(wordCount)
+        }
+
+        return BookUiItem(
+            book = this,
+            displayTags = tagList.toImmutableList()
+        )
+    }
+}
+
+/**
+ * 理想实现:专为 UI 设计的状态类
+ */
+@Stable
+data class BookUiItem(
+    val book: BookShelfItem,
+    val displayTags: ImmutableList<String>
+) {
+    fun matches(key: String): Boolean {
+        return book.name.contains(key, true) ||
+                book.author.contains(key, true) ||
+                book.originName.contains(key, true) ||
+                displayTags.any { it.contains(key, true) }
+    }
 }
 
 fun BookShelfItem.toLightBook() = Book(
@@ -64,5 +101,7 @@ fun BookShelfItem.toLightBook() = Book(
     type = type,
     group = group,
     order = order,
-    canUpdate = canUpdate
+    canUpdate = canUpdate,
+    wordCount = wordCount,
+    kind = kind
 )

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

@@ -847,7 +847,7 @@ fun BookshelfScreen(
                             uiState = uiState,
                             selectedBookUrls = selectedBookUrls,
                             canReorderBooks = false,
-                            onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                            onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
                             draggingBooks = null,
                             pendingSavedBooks = null,
                             onDragStarted = {},
@@ -890,7 +890,7 @@ fun BookshelfScreen(
                                     uiState = uiState,
                                     selectedBookUrls = selectedBookUrls,
                                     canReorderBooks = canReorderBooks,
-                                    onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
+                                    onToggleBookSelection = { toggleBookSelection(it.book.bookUrl) },
                                     draggingBooks = if (isSelectedGroup) {
                                         uiState.draggingBooks
                                     } else {
@@ -1241,15 +1241,15 @@ private data class BookshelfEditStickySummary(
 @Composable
 fun BookshelfPage(
     paddingValues: PaddingValues,
-    books: ImmutableList<BookShelfItem>,
+    books: ImmutableList<BookUiItem>,
     uiState: BookshelfUiState,
     selectedBookUrls: ImmutableSet<String>,
     canReorderBooks: Boolean,
-    onToggleBookSelection: (BookShelfItem) -> Unit,
-    draggingBooks: ImmutableList<BookShelfItem>?,
-    pendingSavedBooks: ImmutableList<BookShelfItem>?,
-    onDragStarted: (ImmutableList<BookShelfItem>) -> Unit,
-    onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList<BookShelfItem>) -> Unit,
+    onToggleBookSelection: (BookUiItem) -> Unit,
+    draggingBooks: ImmutableList<BookUiItem>?,
+    pendingSavedBooks: ImmutableList<BookUiItem>?,
+    onDragStarted: (ImmutableList<BookUiItem>) -> Unit,
+    onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList<BookUiItem>) -> Unit,
     onDragFinished: () -> Unit,
     onGlobalSearch: () -> Unit,
     onBookClick: (BookShelfItem) -> Unit,
@@ -1367,15 +1367,15 @@ fun BookshelfPage(
             horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
             showFastScroll = showFastScroll
         ) {
-            items(displayBooks, key = { it.bookUrl }) { book ->
-                val isSelected = selectedBookUrls.contains(book.bookUrl)
+            items(displayBooks, key = { it.book.bookUrl }) { bookUi ->
+                val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl)
                 ReorderableItem(
                     state = reorderableState,
-                    key = book.bookUrl,
+                    key = bookUi.book.bookUrl,
                     enabled = canReorderBooks
                 ) {
                     BookItem(
-                        book = book,
+                        bookUi = bookUi,
                         modifier = Modifier.then(
                             if (canReorderBooks) {
                                 Modifier.longPressDraggableHandle(
@@ -1399,7 +1399,7 @@ fun BookshelfPage(
                         isSelected = isSelected,
                         gridStyle = bookItemGridStyle,
                         isCompact = bookItemIsCompact,
-                        isUpdating = uiState.updatingBooks.contains(book.bookUrl),
+                        isUpdating = uiState.updatingBooks.contains(bookUi.book.bookUrl),
                         titleSmallFont = bookItemTitleSmallFont,
                         titleCenter = bookItemTitleCenter,
                         titleMaxLines = bookItemTitleMaxLines,
@@ -1408,12 +1408,12 @@ fun BookshelfPage(
                         searchKey = uiState.searchKey,
                         sharedTransitionScope = sharedTransitionScope,
                         animatedVisibilityScope = animatedVisibilityScope,
-                        sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(book.bookUrl) else null,
+                        sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(bookUi.book.bookUrl) else null,
                         onClick = {
                             if (uiState.isEditMode) {
-                                onToggleBookSelection(book)
+                                onToggleBookSelection(bookUi)
                             } else {
-                                onBookClick(book)
+                                onBookClick(bookUi.book)
                             }
                         },
                         onLongClick = if (canReorderBooks) {
@@ -1421,9 +1421,9 @@ fun BookshelfPage(
                         } else {
                             {
                                 if (uiState.isEditMode) {
-                                    onToggleBookSelection(book)
+                                    onToggleBookSelection(bookUi)
                                 } else {
-                                    onBookLongClick(book)
+                                    onBookLongClick(bookUi.book)
                                 }
                             }
                         }

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

@@ -33,7 +33,7 @@ sealed interface BookshelfOverlay {
 
 @Stable
 data class BookshelfUiState(
-    override val items: ImmutableList<BookShelfItem> = persistentListOf(),
+    override val items: ImmutableList<BookUiItem> = persistentListOf(),
     override val selectedIds: ImmutableSet<Any> = persistentSetOf(),
     override val searchKey: String = "",
     override val isSearch: Boolean = false,
@@ -41,7 +41,7 @@ data class BookshelfUiState(
     val isInitialLoading: Boolean = true,
     val groups: ImmutableList<BookGroupUi> = persistentListOf(),
     val allGroups: ImmutableList<BookGroupUi> = persistentListOf(),
-    val groupPreviews: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf(),
+    val groupPreviews: ImmutableMap<Long, ImmutableList<BookUiItem>> = persistentMapOf(),
     val groupBookCounts: ImmutableMap<Long, Int> = persistentMapOf(),
     val currentGroupBookCount: Int = 0,
     val allBooksCount: Int = 0,
@@ -61,7 +61,7 @@ data class BookshelfUiState(
     val title: String = "",
     val subtitle: String? = null,
     val currentGroupName: String? = null,
-    val draggingBooks: ImmutableList<BookShelfItem>? = null,
-    val pendingSavedBooks: ImmutableList<BookShelfItem>? = null,
-    val allGroupBooks: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf()
-) : ListUiState<BookShelfItem>
+    val draggingBooks: ImmutableList<BookUiItem>? = null,
+    val pendingSavedBooks: ImmutableList<BookUiItem>? = null,
+    val allGroupBooks: ImmutableMap<Long, ImmutableList<BookUiItem>> = persistentMapOf()
+) : ListUiState<BookUiItem>

+ 50 - 49
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -117,8 +117,8 @@ class BookshelfViewModel(
     private val isInFolderRootFlow = MutableStateFlow(BookshelfConfig.bookGroupStyle == 2)
     private val isRefreshingFlow = MutableStateFlow(false)
     private val bookGroupStyleFlow = MutableStateFlow(BookshelfConfig.bookGroupStyle)
-    private val draggingBooksFlow = MutableStateFlow<List<BookShelfItem>?>(null)
-    private val pendingSavedBooksFlow = MutableStateFlow<List<BookShelfItem>?>(null)
+    private val draggingBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
+    private val pendingSavedBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
     private val isInitialLoadingFlow = MutableStateFlow(true)
 
     private data class BookshelfSortConfig(
@@ -170,7 +170,7 @@ class BookshelfViewModel(
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
 
     private data class GroupPreviewState(
-        val previews: ImmutableMap<Long, ImmutableList<BookShelfItem>>,
+        val previews: ImmutableMap<Long, ImmutableList<BookUiItem>>,
         val counts: ImmutableMap<Long, Int>,
         val allBookCount: Int
     )
@@ -189,7 +189,7 @@ class BookshelfViewModel(
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState())
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    val booksFlow = groupIdFlow
+    val booksFlow: Flow<List<BookUiItem>> = groupIdFlow
         .flatMapLatest { groupId ->
             combine(
                 appDb.bookDao.flowBookShelfByGroup(groupId),
@@ -200,12 +200,12 @@ class BookshelfViewModel(
                     list,
                     groups.find { it.groupId == groupId },
                     sortConfig
-                )
+                ).map { it.toUiItem() }
             }
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    private val allGroupBooksFlow: StateFlow<Map<Long, List<BookShelfItem>>> = combine(
+    private val allGroupBooksFlow: StateFlow<Map<Long, List<BookUiItem>>> = combine(
         groupsFlow, sortConfigFlow
     ) { groups, sortConfig ->
         groups to sortConfig
@@ -215,7 +215,7 @@ class BookshelfViewModel(
         } else {
             val flows = groups.map { group ->
                 appDb.bookDao.flowBookShelfByGroup(group.groupId).map { books ->
-                    group.groupId to sortBooks(books, group, sortConfig)
+                    group.groupId to sortBooks(books, group, sortConfig).map { it.toUiItem() }
                 }
             }
             combine(flows) { it.toMap() }
@@ -224,13 +224,13 @@ class BookshelfViewModel(
         .flowOn(Dispatchers.Default)
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
 
-    private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookShelfItem>>> =
+    private val allGroupBooksImmutableFlow: StateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>> =
         allGroupBooksFlow.map { map ->
             map.mapValues { it.value.toImmutableList() }.toImmutableMap()
         }.flowOn(Dispatchers.Default)
             .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentMapOf())
 
-    private val visibleBooksFlow = combine(
+    private val visibleBooksFlow: Flow<List<BookUiItem>> = combine(
         booksFlow,
         searchKeyFlow,
         searchModeFlow
@@ -254,7 +254,7 @@ class BookshelfViewModel(
         selectedBookUrlsFlow,
         visibleBooksFlow
     ) { selectedBookUrls, visibleBooks ->
-        val visibleBookUrls = visibleBooks.mapTo(hashSetOf()) { it.bookUrl }
+        val visibleBookUrls = visibleBooks.mapTo(hashSetOf()) { it.book.bookUrl }
         selectedBookUrls.intersect(visibleBookUrls)
     }.distinctUntilChanged()
 
@@ -281,11 +281,11 @@ class BookshelfViewModel(
                 }
                 val previewFlow = appDb.bookDao.flowGroupPreview(group.groupId)
                 combine(countFlow, previewFlow) { count, preview ->
-                    Triple(group.groupId, count, preview)
+                    Triple(group.groupId, count, preview.map { it.toUiItem() })
                 }
             }
             combine(groupFlows) { results ->
-                var previews = persistentMapOf<Long, ImmutableList<BookShelfItem>>()
+                var previews = persistentMapOf<Long, ImmutableList<BookUiItem>>()
                 var counts = persistentMapOf<Long, Int>()
                 var allBookCount = 0
                 results.forEach { (groupId, count, preview) ->
@@ -344,8 +344,8 @@ class BookshelfViewModel(
         val isInFolderRoot: Boolean,
         val isRefreshing: Boolean,
         val bookGroupStyle: Int,
-        val draggingBooks: List<BookShelfItem>?,
-        val pendingSavedBooks: List<BookShelfItem>?
+        val draggingBooks: List<BookUiItem>?,
+        val pendingSavedBooks: List<BookUiItem>?
     )
 
     private val editStateFlow = combine(
@@ -387,7 +387,8 @@ class BookshelfViewModel(
         GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
     )
 
-    private val allGroupBooksStateFlow = MutableStateFlow<ImmutableMap<Long, ImmutableList<BookShelfItem>>>(
+    private val allGroupBooksStateFlow =
+        MutableStateFlow<ImmutableMap<Long, ImmutableList<BookUiItem>>>(
         persistentMapOf()
     )
 
@@ -411,7 +412,7 @@ class BookshelfViewModel(
     }
 
     private data class BookshelfDataCore(
-        val books: List<BookShelfItem>,
+        val books: List<BookUiItem>,
         val groups: List<BookGroup>,
         val allGroups: List<BookGroup>,
         val previews: GroupPreviewState,
@@ -419,12 +420,12 @@ class BookshelfViewModel(
     )
 
     private data class BookshelfDataState(
-        val books: List<BookShelfItem>,
+        val books: List<BookUiItem>,
         val groups: List<BookGroupUi>,
         val allGroups: List<BookGroupUi>,
         val previews: GroupPreviewState,
         val internal: InternalState,
-        val allGroupBooks: ImmutableMap<Long, ImmutableList<BookShelfItem>>
+        val allGroupBooks: ImmutableMap<Long, ImmutableList<BookUiItem>>
     )
 
     val uiState: StateFlow<BookshelfUiState> = combine(
@@ -540,14 +541,14 @@ class BookshelfViewModel(
     }
 
     private fun filterBooks(
-        books: List<BookShelfItem>,
+        books: List<BookUiItem>,
         searchKey: String,
         isSearchMode: Boolean
-    ): List<BookShelfItem> {
+    ): List<BookUiItem> {
         return if (!isSearchMode || searchKey.isBlank()) {
             books
         } else {
-            books.filter { it.matchesSearchKey(searchKey) }
+            books.filter { it.matches(searchKey) }
         }
     }
 
@@ -673,11 +674,11 @@ class BookshelfViewModel(
     }
 
     fun selectAllVisible() {
-        selectedBookUrlsFlow.value = uiState.value.items.mapTo(hashSetOf()) { it.bookUrl }
+        selectedBookUrlsFlow.value = uiState.value.items.mapTo(hashSetOf()) { it.book.bookUrl }
     }
 
     fun invertVisibleSelection() {
-        val visibleBookUrls = uiState.value.items.mapTo(hashSetOf()) { it.bookUrl }
+        val visibleBookUrls = uiState.value.items.mapTo(hashSetOf()) { it.book.bookUrl }
         selectedBookUrlsFlow.value = visibleBookUrls - selectedBookUrlsFlow.value
     }
 
@@ -719,13 +720,13 @@ class BookshelfViewModel(
         }
     }
 
-    fun saveBookOrder(reorderedBooks: List<BookShelfItem>) {
+    fun saveBookOrder(reorderedBooks: List<BookUiItem>) {
         if (reorderedBooks.isEmpty()) return
         val isDescending = BookshelfConfig.bookshelfSortOrder == 1
         val maxOrder = reorderedBooks.size
         execute {
-            val updates = reorderedBooks.mapIndexedNotNull { index, book ->
-                appDb.bookDao.getBook(book.bookUrl)?.apply {
+            val updates = reorderedBooks.mapIndexedNotNull { index, bookUi ->
+                appDb.bookDao.getBook(bookUi.book.bookUrl)?.apply {
                     order = if (isDescending) maxOrder - index else index + 1
                 }
             }
@@ -756,19 +757,19 @@ class BookshelfViewModel(
         }
     }
 
-    fun refreshBooks(books: List<BookShelfItem>) {
+    fun refreshBooks(books: List<BookUiItem>) {
         if (isRefreshingFlow.value) return
         isRefreshingFlow.value = true
         val limit = BookshelfConfig.bookshelfRefreshingLimit
         val list = if (limit > 0) books.take(limit) else books
-        enqueueTocUpdate(list, resetRefreshWhenIdle = true)
+        enqueueTocUpdate(list.map { it.book }, resetRefreshWhenIdle = true)
     }
 
-    fun startDraggingBooks(books: List<BookShelfItem>) {
+    fun startDraggingBooks(books: List<BookUiItem>) {
         draggingBooksFlow.value = books
     }
 
-    fun moveDraggingBook(fromIndex: Int, toIndex: Int, fallbackBooks: List<BookShelfItem>) {
+    fun moveDraggingBook(fromIndex: Int, toIndex: Int, fallbackBooks: List<BookUiItem>) {
         if (fromIndex == toIndex) return
         val sourceBooks = draggingBooksFlow.value ?: fallbackBooks
         if (fromIndex !in sourceBooks.indices || toIndex !in sourceBooks.indices) return
@@ -778,19 +779,19 @@ class BookshelfViewModel(
     }
 
     fun finishDraggingBooks() {
-        val reorderedBooks = draggingBooksFlow.value ?: return
-        pendingSavedBooksFlow.value = reorderedBooks
+        val reorderedUiBooks = draggingBooksFlow.value ?: return
+        pendingSavedBooksFlow.value = reorderedUiBooks
         draggingBooksFlow.value = null
-        saveBookOrder(reorderedBooks)
+        saveBookOrder(reorderedUiBooks)
     }
 
-    private fun syncDragState(books: List<BookShelfItem>, canReorderBooks: Boolean) {
+    private fun syncDragState(books: List<BookUiItem>, canReorderBooks: Boolean) {
         if (!canReorderBooks) {
             clearDragState()
             return
         }
         val pending = pendingSavedBooksFlow.value ?: return
-        if (books.map { it.bookUrl } == pending.map { it.bookUrl }) {
+        if (books.map { it.book.bookUrl } == pending.map { it.book.bookUrl }) {
             pendingSavedBooksFlow.value = null
         }
     }
@@ -811,10 +812,10 @@ class BookshelfViewModel(
         }
     }
 
-    fun upToc(books: List<BookShelfItem>) {
+    fun upToc(books: List<BookUiItem>) {
         val limit = BookshelfConfig.bookshelfRefreshingLimit
         val list = if (limit > 0) books.take(limit) else books
-        enqueueTocUpdate(list, resetRefreshWhenIdle = false)
+        enqueueTocUpdate(list.map { it.book }, resetRefreshWhenIdle = false)
     }
 
     private fun enqueueTocUpdate(
@@ -1088,7 +1089,7 @@ class BookshelfViewModel(
         }
     }
 
-    fun exportToUri(uri: Uri, items: List<BookShelfItem>) {
+    fun exportToUri(uri: Uri, items: List<BookUiItem>) {
         execute {
             context.contentResolver.openOutputStream(uri)?.use { out ->
                 val writer = JsonWriter(OutputStreamWriter(out, "UTF-8"))
@@ -1096,11 +1097,11 @@ class BookshelfViewModel(
                 writer.beginArray()
                 items.forEach {
                     val bookMap = hashMapOf<String, String?>()
-                    bookMap["name"] = it.name
-                    bookMap["author"] = it.author
+                    bookMap["name"] = it.book.name
+                    bookMap["author"] = it.book.author
                     // 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)
+                    val fullBook = appDb.bookDao.getBook(it.book.bookUrl)
                     bookMap["intro"] = fullBook?.getDisplayIntro()
                     GSON.toJson(bookMap, bookMap::class.java, writer)
                 }
@@ -1114,14 +1115,14 @@ class BookshelfViewModel(
         }
     }
 
-    fun uploadBookshelf(items: List<BookShelfItem>) {
+    fun uploadBookshelf(items: List<BookUiItem>) {
         execute {
             val json = withContext(Dispatchers.Default) {
                 val list = items.map {
                     val bookMap = hashMapOf<String, String?>()
-                    bookMap["name"] = it.name
-                    bookMap["author"] = it.author
-                    val fullBook = appDb.bookDao.getBook(it.bookUrl)
+                    bookMap["name"] = it.book.name
+                    bookMap["author"] = it.book.author
+                    val fullBook = appDb.bookDao.getBook(it.book.bookUrl)
                     bookMap["intro"] = fullBook?.getDisplayIntro()
                     bookMap
                 }
@@ -1149,7 +1150,7 @@ class BookshelfViewModel(
         }
     }
 
-    fun exportBookshelf(items: List<BookShelfItem>?, success: (file: File) -> Unit) {
+    fun exportBookshelf(items: List<BookUiItem>?, success: (file: File) -> Unit) {
         execute {
             items?.let {
                 val path = "${context.filesDir}/books.json"
@@ -1161,9 +1162,9 @@ class BookshelfViewModel(
                     writer.beginArray()
                     items.forEach {
                         val bookMap = hashMapOf<String, String?>()
-                        bookMap["name"] = it.name
-                        bookMap["author"] = it.author
-                        val fullBook = appDb.bookDao.getBook(it.bookUrl)
+                        bookMap["name"] = it.book.name
+                        bookMap["author"] = it.book.author
+                        val fullBook = appDb.bookDao.getBook(it.book.bookUrl)
                         bookMap["intro"] = fullBook?.getDisplayIntro()
                         GSON.toJson(bookMap, bookMap::class.java, writer)
                     }