From 207b00189f4084ec3717e4d22911b9c479b536d8 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sun, 17 May 2026 02:00:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E7=AD=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/data/dao/BookDao.kt | 68 ++++++++----- .../java/io/legado/app/data/entities/Book.kt | 3 + .../io/legado/app/help/book/BookExtensions.kt | 45 ++++++++- .../legado/app/model/localBook/LocalBook.kt | 7 +- .../io/legado/app/model/localBook/TextFile.kt | 2 + .../app/ui/book/info/BookInfoViewModel.kt | 17 +--- .../legado/app/ui/main/bookshelf/BookItem.kt | 29 +++--- .../app/ui/main/bookshelf/BookShelfItem.kt | 45 ++++++++- .../app/ui/main/bookshelf/BookshelfScreen.kt | 36 +++---- .../app/ui/main/bookshelf/BookshelfUiState.kt | 12 +-- .../ui/main/bookshelf/BookshelfViewModel.kt | 99 ++++++++++--------- 11 files changed, 230 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt index 7f60cf61e..2729c0dae 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index d653c7125..0da6dc54b 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/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 } diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index c95b6778f..766a7d14f 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 2ca3965f1..613ea48f9 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/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() } } diff --git a/app/src/main/java/io/legado/app/model/localBook/TextFile.kt b/app/src/main/java/io/legado/app/model/localBook/TextFile.kt index 7766e3734..40abe6822 100644 --- a/app/src/main/java/io/legado/app/model/localBook/TextFile.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index 46ff441b7..d80faadd4 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 11e35c095..60aa48622 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/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, + books: List, 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, + previewBooks: List, countText: String? = null, gridStyle: Int = 0, titleSmallFont: Boolean = false, @@ -443,7 +442,7 @@ fun BookGroupItemGrid( @Composable fun BookGroupItemList( group: BookGroupUi, - previewBooks: List, + previewBooks: List, 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, + previewBooks: List, 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() diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt index 19e02b17f..70acf40cd 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt +++ b/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() + 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 +) { + 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 ) 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 5442750a6..d90bbaa1a 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 @@ -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, + books: ImmutableList, uiState: BookshelfUiState, selectedBookUrls: ImmutableSet, canReorderBooks: Boolean, - onToggleBookSelection: (BookShelfItem) -> Unit, - draggingBooks: ImmutableList?, - pendingSavedBooks: ImmutableList?, - onDragStarted: (ImmutableList) -> Unit, - onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList) -> Unit, + onToggleBookSelection: (BookUiItem) -> Unit, + draggingBooks: ImmutableList?, + pendingSavedBooks: ImmutableList?, + onDragStarted: (ImmutableList) -> Unit, + onMoveBook: (fromIndex: Int, toIndex: Int, currentBooks: ImmutableList) -> 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) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt index 2f18d1549..06c6c9f0b 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt +++ b/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 = persistentListOf(), + override val items: ImmutableList = persistentListOf(), override val selectedIds: ImmutableSet = persistentSetOf(), override val searchKey: String = "", override val isSearch: Boolean = false, @@ -41,7 +41,7 @@ data class BookshelfUiState( val isInitialLoading: Boolean = true, val groups: ImmutableList = persistentListOf(), val allGroups: ImmutableList = persistentListOf(), - val groupPreviews: ImmutableMap> = persistentMapOf(), + val groupPreviews: ImmutableMap> = persistentMapOf(), val groupBookCounts: ImmutableMap = 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? = null, - val pendingSavedBooks: ImmutableList? = null, - val allGroupBooks: ImmutableMap> = persistentMapOf() -) : ListUiState + val draggingBooks: ImmutableList? = null, + val pendingSavedBooks: ImmutableList? = null, + val allGroupBooks: ImmutableMap> = persistentMapOf() +) : ListUiState 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 8d94ea36d..c1665e5ec 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 @@ -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?>(null) - private val pendingSavedBooksFlow = MutableStateFlow?>(null) + private val draggingBooksFlow = MutableStateFlow?>(null) + private val pendingSavedBooksFlow = MutableStateFlow?>(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>, + val previews: ImmutableMap>, val counts: ImmutableMap, val allBookCount: Int ) @@ -189,7 +189,7 @@ class BookshelfViewModel( .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState()) @OptIn(ExperimentalCoroutinesApi::class) - val booksFlow = groupIdFlow + val booksFlow: Flow> = 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>> = combine( + private val allGroupBooksFlow: StateFlow>> = 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>> = + private val allGroupBooksImmutableFlow: StateFlow>> = 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> = 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>() + var previews = persistentMapOf>() var counts = persistentMapOf() 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?, - val pendingSavedBooks: List? + val draggingBooks: List?, + val pendingSavedBooks: List? ) private val editStateFlow = combine( @@ -387,7 +387,8 @@ class BookshelfViewModel( GroupPreviewState(persistentMapOf(), persistentMapOf(), 0) ) - private val allGroupBooksStateFlow = MutableStateFlow>>( + private val allGroupBooksStateFlow = + MutableStateFlow>>( persistentMapOf() ) @@ -411,7 +412,7 @@ class BookshelfViewModel( } private data class BookshelfDataCore( - val books: List, + val books: List, val groups: List, val allGroups: List, val previews: GroupPreviewState, @@ -419,12 +420,12 @@ class BookshelfViewModel( ) private data class BookshelfDataState( - val books: List, + val books: List, val groups: List, val allGroups: List, val previews: GroupPreviewState, val internal: InternalState, - val allGroupBooks: ImmutableMap> + val allGroupBooks: ImmutableMap> ) val uiState: StateFlow = combine( @@ -540,14 +541,14 @@ class BookshelfViewModel( } private fun filterBooks( - books: List, + books: List, searchKey: String, isSearchMode: Boolean - ): List { + ): List { 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) { + fun saveBookOrder(reorderedBooks: List) { 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) { + fun refreshBooks(books: List) { 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) { + fun startDraggingBooks(books: List) { draggingBooksFlow.value = books } - fun moveDraggingBook(fromIndex: Int, toIndex: Int, fallbackBooks: List) { + fun moveDraggingBook(fromIndex: Int, toIndex: Int, fallbackBooks: List) { 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, canReorderBooks: Boolean) { + private fun syncDragState(books: List, 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) { + fun upToc(books: List) { 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) { + fun exportToUri(uri: Uri, items: List) { 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() - 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) { + fun uploadBookshelf(items: List) { execute { val json = withContext(Dispatchers.Default) { val list = items.map { val bookMap = hashMapOf() - 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?, success: (file: File) -> Unit) { + fun exportBookshelf(items: List?, 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() - 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) }