[新增] 阅读记录时间线视图现在可以显示一次记录的最后阅读章节

This commit is contained in:
HapeLee
2025-11-30 03:20:41 +08:00
parent de9e9b21c2
commit ec90fc0121
7 changed files with 68 additions and 46 deletions
@@ -43,4 +43,13 @@ interface BookChapterDao {
@Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
fun upWordCount(bookUrl: String, url: String, wordCount: String)
/**
* 根据书籍的唯一标识 bookUrl 和章节索引 index 查找章节标题。
*/
@Query("""
SELECT title FROM chapters
WHERE bookUrl = :bookUrl
AND `index` = :chapterIndex
""")
suspend fun getChapterTitleByUrlAndIndex(bookUrl: String, chapterIndex: Int): String?
}
@@ -9,7 +9,8 @@ import org.koin.dsl.module
val readRecordModule = module {
single { get<AppDatabase>().readRecordDao }
single { get<AppDatabase>().bookDao }
single { get<AppDatabase>().bookChapterDao }
single { ReadRecordRepository(get()) }
viewModel { ReadRecordViewModel(get(), get()) }
viewModel { ReadRecordViewModel(get(), get(), get()) }
}
@@ -312,7 +312,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
bookName = currentBookName,
startTime = readStartTime,
endTime = readStartTime,
words = 0
words = durChapterIndex.toLong()
)
}
}
@@ -327,12 +327,9 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
return
}
var wordChange = currentLength - lastReadLength
if (wordChange < 0) wordChange = 0
currentActiveSession = currentActiveSession!!.copy(
endTime = endTime,
words = currentActiveSession!!.words + wordChange
words = durChapterIndex.toLong()
)
readStartTime = endTime
@@ -158,7 +158,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
bookName = currentBookName,
startTime = readStartTime,
endTime = readStartTime,
words = 0
words = durChapterIndex.toLong()
)
}
}
@@ -173,12 +173,9 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
return
}
var wordChange = currentLength - lastReadLength
if (wordChange < 0) wordChange = 0
currentActiveSession = currentActiveSession!!.copy(
endTime = endTime,
words = currentActiveSession!!.words + wordChange
words = durChapterIndex.toLong()
)
readStartTime = endTime
@@ -337,7 +337,6 @@ fun ExploreBookItem(
Text(
text = book.author,
style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1
)
@@ -365,7 +364,7 @@ fun ExploreBookItem(
Text(
text = intro,
style = MaterialTheme.typography.bodySmall,
color = Color.DarkGray,
color = Color.Gray,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
@@ -52,6 +52,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import cn.hutool.core.date.DateUtil
@@ -272,8 +273,7 @@ fun ReadRecordScreen(
}
DisplayMode.TIMELINE -> {
state.timelineRecords.forEach { (date, sessions) ->
val dailyTotalTime = sessions.sumOf { it.endTime - it.startTime }
stickyHeader { DateHeader(date, dailyTotalTime) }
stickyHeader { DateHeader(date) }
val timelineItems = sessions.mapIndexed { index, session ->
val showHeader = true
@@ -360,14 +360,15 @@ fun TimelineSessionItem(
) {
val session = item.session
var coverPath by remember { mutableStateOf<String?>(null) }
var chapterTitle by remember { mutableStateOf<String?>("加载中...") }
LaunchedEffect(session.bookName) {
coverPath = viewModel.getBookCover(session.bookName)
val title = viewModel.getChapterTitle(session.bookName, session.words)
chapterTitle = title ?: "${session.words}"
}
val startTimeText = DateUtil.format(Date(session.startTime), "HH:mm")
val endTimeText = DateUtil.format(Date(session.endTime), "HH:mm")
val duration = session.endTime - session.startTime
val nodeRadius = 4.dp
val lineWidth = 2.dp
@@ -380,7 +381,6 @@ fun TimelineSessionItem(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 0.dp)
.clickable { onBookClick(session.bookName) }
.drawBehind {
val x = timelineX.toPx()
@@ -399,7 +399,6 @@ fun TimelineSessionItem(
radius = nodeRadius.toPx(),
center = Offset(x, cy)
)
}
) {
Row(
@@ -418,28 +417,26 @@ fun TimelineSessionItem(
)
}
Column(modifier = Modifier.weight(1f)) {
if (item.showHeader) {
Row(verticalAlignment = Alignment.CenterVertically) {
Cover(coverPath)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = session.bookName,
style = MaterialTheme.typography.titleMedium,
maxLines = 2
)
}
Spacer(modifier = Modifier.height(6.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Cover(coverPath)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
"时长: ${formatDuring(duration)}",
text = session.bookName,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
minLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = chapterTitle.orEmpty(),
style = MaterialTheme.typography.bodySmall,
color = Color.Gray
color = Color.Gray,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Text(
// "字数: ${session.words}",
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
}
}
}
@@ -494,10 +491,9 @@ fun ReadRecordItem(
@Composable
fun DateHeader(
date: String,
dailyTotalTime: Long
dailyTotalTime: Long? = null
) {
val dateText = formatFriendlyDate(date)
val totalTimeText = "已读 ${formatDuring(dailyTotalTime)}"
Surface(
modifier = Modifier.fillMaxWidth()
) {
@@ -509,11 +505,13 @@ fun DateHeader(
color = MaterialTheme.colorScheme.secondary
)
Text(
text = totalTimeText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
)
dailyTotalTime?.let { total ->
Text(
text = "已读 ${formatDuring(total)}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
@@ -2,6 +2,7 @@ package io.legado.app.ui.book.readRecord
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail
@@ -36,7 +37,8 @@ enum class DisplayMode {
class ReadRecordViewModel(
private val repository: ReadRecordRepository,
private val bookDao: BookDao
private val bookDao: BookDao,
private val bookChapterDao: BookChapterDao
) : ViewModel() {
private val _displayMode = MutableStateFlow(DisplayMode.AGGREGATE)
val displayMode = _displayMode.asStateFlow()
@@ -149,6 +151,25 @@ class ReadRecordViewModel(
return mergedList
}
suspend fun getChapterTitle(
bookName: String,
chapterIndexLong: Long
): String? {
val chapterIndex = chapterIndexLong.toInt()
val book = withContext(Dispatchers.IO) {
bookDao.findByName(bookName).firstOrNull()
}
val bookUrl = book?.bookUrl
if (bookUrl.isNullOrEmpty()) {
return null
}
return withContext(Dispatchers.IO) {
bookChapterDao.getChapterTitleByUrlAndIndex(bookUrl, chapterIndex)
}
}
fun deleteDetail(detail: ReadRecordDetail) {
viewModelScope.launch {
repository.deleteDetail(detail)