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

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") @Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
fun upWordCount(bookUrl: String, url: String, wordCount: String) 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 { val readRecordModule = module {
single { get<AppDatabase>().readRecordDao } single { get<AppDatabase>().readRecordDao }
single { get<AppDatabase>().bookDao } single { get<AppDatabase>().bookDao }
single { get<AppDatabase>().bookChapterDao }
single { ReadRecordRepository(get()) } single { ReadRecordRepository(get()) }
viewModel { ReadRecordViewModel(get(), get()) } viewModel { ReadRecordViewModel(get(), get(), get()) }
} }
@@ -312,7 +312,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
bookName = currentBookName, bookName = currentBookName,
startTime = readStartTime, startTime = readStartTime,
endTime = readStartTime, endTime = readStartTime,
words = 0 words = durChapterIndex.toLong()
) )
} }
} }
@@ -327,12 +327,9 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
return return
} }
var wordChange = currentLength - lastReadLength
if (wordChange < 0) wordChange = 0
currentActiveSession = currentActiveSession!!.copy( currentActiveSession = currentActiveSession!!.copy(
endTime = endTime, endTime = endTime,
words = currentActiveSession!!.words + wordChange words = durChapterIndex.toLong()
) )
readStartTime = endTime readStartTime = endTime
@@ -158,7 +158,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
bookName = currentBookName, bookName = currentBookName,
startTime = readStartTime, startTime = readStartTime,
endTime = readStartTime, endTime = readStartTime,
words = 0 words = durChapterIndex.toLong()
) )
} }
} }
@@ -173,12 +173,9 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
return return
} }
var wordChange = currentLength - lastReadLength
if (wordChange < 0) wordChange = 0
currentActiveSession = currentActiveSession!!.copy( currentActiveSession = currentActiveSession!!.copy(
endTime = endTime, endTime = endTime,
words = currentActiveSession!!.words + wordChange words = durChapterIndex.toLong()
) )
readStartTime = endTime readStartTime = endTime
@@ -337,7 +337,6 @@ fun ExploreBookItem(
Text( Text(
text = book.author, text = book.author,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color.Gray,
maxLines = 1 maxLines = 1
) )
@@ -365,7 +364,7 @@ fun ExploreBookItem(
Text( Text(
text = intro, text = intro,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = Color.DarkGray, color = Color.Gray,
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@@ -52,6 +52,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import cn.hutool.core.date.DateUtil import cn.hutool.core.date.DateUtil
@@ -272,8 +273,7 @@ fun ReadRecordScreen(
} }
DisplayMode.TIMELINE -> { DisplayMode.TIMELINE -> {
state.timelineRecords.forEach { (date, sessions) -> state.timelineRecords.forEach { (date, sessions) ->
val dailyTotalTime = sessions.sumOf { it.endTime - it.startTime } stickyHeader { DateHeader(date) }
stickyHeader { DateHeader(date, dailyTotalTime) }
val timelineItems = sessions.mapIndexed { index, session -> val timelineItems = sessions.mapIndexed { index, session ->
val showHeader = true val showHeader = true
@@ -360,14 +360,15 @@ fun TimelineSessionItem(
) { ) {
val session = item.session val session = item.session
var coverPath by remember { mutableStateOf<String?>(null) } var coverPath by remember { mutableStateOf<String?>(null) }
var chapterTitle by remember { mutableStateOf<String?>("加载中...") }
LaunchedEffect(session.bookName) { LaunchedEffect(session.bookName) {
coverPath = viewModel.getBookCover(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 endTimeText = DateUtil.format(Date(session.endTime), "HH:mm")
val duration = session.endTime - session.startTime
val nodeRadius = 4.dp val nodeRadius = 4.dp
val lineWidth = 2.dp val lineWidth = 2.dp
@@ -380,7 +381,6 @@ fun TimelineSessionItem(
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 0.dp)
.clickable { onBookClick(session.bookName) } .clickable { onBookClick(session.bookName) }
.drawBehind { .drawBehind {
val x = timelineX.toPx() val x = timelineX.toPx()
@@ -399,7 +399,6 @@ fun TimelineSessionItem(
radius = nodeRadius.toPx(), radius = nodeRadius.toPx(),
center = Offset(x, cy) center = Offset(x, cy)
) )
} }
) { ) {
Row( Row(
@@ -418,28 +417,26 @@ fun TimelineSessionItem(
) )
} }
Column(modifier = Modifier.weight(1f)) { Row(
if (item.showHeader) { verticalAlignment = Alignment.CenterVertically
Row(verticalAlignment = Alignment.CenterVertically) { ) {
Cover(coverPath) Cover(coverPath)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column {
Text( Text(
text = session.bookName, text = session.bookName,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
maxLines = 2 maxLines = 2,
minLines = 2,
overflow = TextOverflow.Ellipsis
) )
}
Spacer(modifier = Modifier.height(6.dp))
Text( Text(
"时长: ${formatDuring(duration)}", text = chapterTitle.orEmpty(),
style = MaterialTheme.typography.bodySmall, 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 @Composable
fun DateHeader( fun DateHeader(
date: String, date: String,
dailyTotalTime: Long dailyTotalTime: Long? = null
) { ) {
val dateText = formatFriendlyDate(date) val dateText = formatFriendlyDate(date)
val totalTimeText = "已读 ${formatDuring(dailyTotalTime)}"
Surface( Surface(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
@@ -509,14 +505,16 @@ fun DateHeader(
color = MaterialTheme.colorScheme.secondary color = MaterialTheme.colorScheme.secondary
) )
dailyTotalTime?.let { total ->
Text( Text(
text = totalTimeText, text = "已读 ${formatDuring(total)}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
} }
} }
} }
}
@Composable @Composable
fun CalendarSection( fun CalendarSection(
@@ -2,6 +2,7 @@ package io.legado.app.ui.book.readRecord
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao import io.legado.app.data.dao.BookDao
import io.legado.app.data.entities.readRecord.ReadRecord import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail import io.legado.app.data.entities.readRecord.ReadRecordDetail
@@ -36,7 +37,8 @@ enum class DisplayMode {
class ReadRecordViewModel( class ReadRecordViewModel(
private val repository: ReadRecordRepository, private val repository: ReadRecordRepository,
private val bookDao: BookDao private val bookDao: BookDao,
private val bookChapterDao: BookChapterDao
) : ViewModel() { ) : ViewModel() {
private val _displayMode = MutableStateFlow(DisplayMode.AGGREGATE) private val _displayMode = MutableStateFlow(DisplayMode.AGGREGATE)
val displayMode = _displayMode.asStateFlow() val displayMode = _displayMode.asStateFlow()
@@ -149,6 +151,25 @@ class ReadRecordViewModel(
return mergedList 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) { fun deleteDetail(detail: ReadRecordDetail) {
viewModelScope.launch { viewModelScope.launch {
repository.deleteDetail(detail) repository.deleteDetail(detail)