标签问题

This commit is contained in:
HapeLee
2026-05-17 02:00:48 +08:00
parent f0bbd4697f
commit 207b00189f
11 changed files with 230 additions and 133 deletions
@@ -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
@@ -355,6 +355,9 @@ data class Book(
newBook.type = type
}
newBook.readConfig = readConfig
if (newBook.wordCount.isNullOrBlank()) {
newBook.wordCount = wordCount
}
return newBook
}
@@ -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)
@@ -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()
}
}
@@ -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
@@ -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
@@ -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()
@@ -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
)
@@ -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)
}
}
}
@@ -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>
@@ -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)
}