[新增] 阅读记录时间线视图现在可以显示一次记录的最后阅读章节
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user