优化阅读记录总览(在阅读记录的成就卡片进入,稳定后会加入主页面)

This commit is contained in:
HapeLee
2026-05-13 01:58:31 +08:00
parent b3c4f7d09f
commit c1f7faf050
7 changed files with 119 additions and 41 deletions
@@ -69,6 +69,18 @@ class GetReadRecordOverviewUseCase {
val dailyTimeData = if (period == ReadPeriod.ALL) { val dailyTimeData = if (period == ReadPeriod.ALL) {
emptyList() emptyList()
} else if (period == ReadPeriod.YEAR) {
val dateToTime = filteredDetails.groupBy {
LocalDate.parse(it.date, DateTimeFormatter.ISO_LOCAL_DATE).with(TemporalAdjusters.firstDayOfMonth())
}.mapValues { it.value.sumOf { d -> d.readTime } }
val monthList = mutableListOf<Pair<LocalDate, Long>>()
var curr = startDate.with(TemporalAdjusters.firstDayOfMonth())
while (!curr.isAfter(endDate)) {
monthList.add(curr to (dateToTime[curr] ?: 0L))
curr = curr.plusMonths(1)
}
monthList
} else { } else {
val dateToTime = filteredDetails.groupBy { it.date } val dateToTime = filteredDetails.groupBy { it.date }
.mapValues { it.value.sumOf { d -> d.readTime } } .mapValues { it.value.sumOf { d -> d.readTime } }
@@ -29,7 +29,7 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.cover.CoilBookCover
import io.legado.app.ui.widget.components.heatmap.HeatmapMode import io.legado.app.ui.widget.components.heatmap.HeatmapMode
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
@@ -223,8 +223,12 @@ fun TopReadingListCard(
.fillMaxWidth() .fillMaxWidth()
.adaptiveHorizontalPadding(vertical = 8.dp) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(vertical = 16.dp)) {
AppText("阅读时长榜", style = LegadoTheme.typography.titleMedium) AppText(
modifier = Modifier.padding(horizontal = 16.dp),
text = "阅读时长榜",
style = LegadoTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
topBooks.forEachIndexed { index, book -> topBooks.forEachIndexed { index, book ->
@@ -237,7 +241,7 @@ fun TopReadingListCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onBookClick(book.bookName, book.bookAuthor) } .clickable { onBookClick(book.bookName, book.bookAuthor) }
.padding(vertical = 8.dp), .padding(vertical = 8.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AppText( AppText(
@@ -247,7 +251,12 @@ fun TopReadingListCard(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
) )
Cover(path = coverPath, modifier = Modifier.size(40.dp, 56.dp)) CoilBookCover(
name = book.bookName,
author = book.bookAuthor,
path = coverPath,
modifier = Modifier.width(40.dp)
)
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
AppText( AppText(
@@ -264,6 +273,7 @@ fun TopReadingListCard(
) )
} }
AppText( AppText(
modifier = Modifier.padding(end = 8.dp),
text = ReadRecordFormatter.formatDuration(book.readTime), text = ReadRecordFormatter.formatDuration(book.readTime),
style = LegadoTheme.typography.bodySmall, style = LegadoTheme.typography.bodySmall,
color = LegadoTheme.colorScheme.primary color = LegadoTheme.colorScheme.primary
@@ -355,10 +365,14 @@ fun CalendarDayCell(
.background(LegadoTheme.colorScheme.surfaceVariant), .background(LegadoTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (coverPath != null) { if (topBook != null) {
Cover( CoilBookCover(
name = topBook.first,
author = topBook.second,
path = coverPath, path = coverPath,
modifier = Modifier.fillMaxSize().alpha(0.4f) modifier = Modifier
.fillMaxSize()
.alpha(0.4f)
) )
} }
@@ -83,7 +83,7 @@ import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.cover.CoilBookCover
import io.legado.app.ui.widget.components.heatmap.HEATMAP_CALENDAR_TITLE import io.legado.app.ui.widget.components.heatmap.HEATMAP_CALENDAR_TITLE
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarEndAction import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarEndAction
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarStartAction import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarStartAction
@@ -728,7 +728,12 @@ fun LatestReadItem(
.adaptiveHorizontalPadding(vertical = 8.dp), .adaptiveHorizontalPadding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(coverPath) CoilBookCover(
name = record.bookName,
author = record.bookAuthor,
path = coverPath,
modifier = Modifier.width(44.dp)
)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -839,7 +844,12 @@ fun TimelineSessionItem(
Row( Row(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(coverPath) CoilBookCover(
name = session.bookName,
author = session.bookAuthor,
path = coverPath,
modifier = Modifier.width(44.dp)
)
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column { Column {
AppText( AppText(
@@ -889,7 +899,12 @@ fun ReadRecordItem(
.adaptiveHorizontalPadding(vertical = 8.dp), .adaptiveHorizontalPadding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(coverPath) CoilBookCover(
name = detail.bookName,
author = detail.bookAuthor,
path = coverPath,
modifier = Modifier.width(44.dp)
)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
@@ -1017,24 +1032,25 @@ fun ReadingSummaryCard(
} }
if (bookNamesForCover.isNotEmpty()) { if (bookNamesForCover.isNotEmpty()) {
BookStackView(coverPaths = coverPaths) val combined = bookNamesForCover.zip(coverPaths)
BookStackView(books = combined)
} }
} }
} }
} }
@Composable @Composable
fun BookStackView(coverPaths: List<String?>) { fun BookStackView(books: List<Pair<Pair<String, String>, String?>>) {
val xOffsetStep = 12.dp val xOffsetStep = 12.dp
val stackWidth = 48.dp + (xOffsetStep * (coverPaths.size - 1).coerceAtLeast(0)) val stackWidth = 44.dp + (xOffsetStep * (books.size - 1).coerceAtLeast(0))
Box( Box(
modifier = Modifier modifier = Modifier
.width(stackWidth) .width(stackWidth)
.height(72.dp), .height(64.dp),
contentAlignment = Alignment.CenterStart contentAlignment = Alignment.CenterStart
) { ) {
coverPaths.forEachIndexed { index, path -> books.forEachIndexed { index, (info, path) ->
Box( Box(
modifier = Modifier modifier = Modifier
.padding(start = xOffsetStep * index) .padding(start = xOffsetStep * index)
@@ -1046,7 +1062,12 @@ fun BookStackView(coverPaths: List<String?>) {
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
color = Color.Transparent color = Color.Transparent
) { ) {
Cover(path = path) CoilBookCover(
name = info.first,
author = info.second,
path = path,
modifier = Modifier.width(44.dp)
)
} }
} }
} }
@@ -14,7 +14,6 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.formatReadDuration
import java.time.LocalDate import java.time.LocalDate
@Composable @Composable
@@ -48,7 +47,7 @@ fun ReadingTimeBarChartCard(
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
AppText( AppText(
text = formatReadDuration(maxTime), text = formatChartDuration(maxTime),
style = LegadoTheme.typography.labelSmall, style = LegadoTheme.typography.labelSmall,
fontSize = 8.sp, fontSize = 8.sp,
color = LegadoTheme.colorScheme.onSurfaceVariant color = LegadoTheme.colorScheme.onSurfaceVariant
@@ -76,16 +75,16 @@ fun ReadingTimeBarChartCard(
val showLabel = when (period) { val showLabel = when (period) {
ReadPeriod.DAY -> true ReadPeriod.DAY -> true
ReadPeriod.WEEK -> date.dayOfWeek.value in listOf(2, 4, 6) ReadPeriod.WEEK -> true
ReadPeriod.MONTH -> date.dayOfMonth == 1 || date.dayOfMonth % 7 == 0 || index == data.lastIndex ReadPeriod.MONTH -> date.dayOfMonth == 1 || date.dayOfMonth == 15 || index == data.lastIndex
ReadPeriod.YEAR -> date.dayOfMonth == 1 ReadPeriod.YEAR -> true
else -> false else -> false
} }
val labelText = when (period) { val labelText = when (period) {
ReadPeriod.YEAR -> "${date.monthValue}" ReadPeriod.YEAR -> "${date.monthValue}"
ReadPeriod.WEEK -> when (date.dayOfWeek.value) { ReadPeriod.WEEK -> when (date.dayOfWeek.value) {
2 -> ""; 4 -> ""; 6 -> ""; else -> "" 1 -> ""; 2 -> ""; 3 -> ""; 4 -> ""; 5 -> ""; 6 -> ""; 7 -> ""; else -> ""
} }
else -> date.dayOfMonth.toString() else -> date.dayOfMonth.toString()
} }
@@ -94,20 +93,26 @@ fun ReadingTimeBarChartCard(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.fillMaxHeight(), .fillMaxHeight(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.6f) .weight(1f)
.fillMaxWidth(),
contentAlignment = Alignment.BottomCenter
) {
Box(
modifier = Modifier
.fillMaxWidth(if (period == ReadPeriod.MONTH) 0.8f else 0.6f)
.fillMaxHeight(heightFactor.coerceAtLeast(0.01f)) .fillMaxHeight(heightFactor.coerceAtLeast(0.01f))
.padding(bottom = 4.dp) .padding(horizontal = 1.dp)
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
.background( .background(
if (time > 0) LegadoTheme.colorScheme.primary if (time > 0) LegadoTheme.colorScheme.primary
else LegadoTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) else LegadoTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
) )
) )
}
Box(modifier = Modifier.height(20.dp), contentAlignment = Alignment.TopCenter) { Box(modifier = Modifier.height(20.dp), contentAlignment = Alignment.TopCenter) {
if (showLabel) { if (showLabel) {
@@ -126,3 +131,10 @@ fun ReadingTimeBarChartCard(
} }
} }
} }
private fun formatChartDuration(millis: Long): String {
val totalMinutes = millis / (1000 * 60)
val hours = totalMinutes / 60
val minutes = totalMinutes % 60
return if (hours > 0) "${hours}h${if (minutes > 0) "${minutes}m" else ""}" else "${minutes}m"
}
@@ -16,7 +16,7 @@ import io.legado.app.ui.book.readRecord.ReadRecordFormatter
import io.legado.app.ui.book.readRecord.ReadRecordViewModel import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.book.readRecord.TimelineItem import io.legado.app.ui.book.readRecord.TimelineItem
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.cover.CoilBookCover
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.StringUtils.formatFriendlyDate import io.legado.app.utils.StringUtils.formatFriendlyDate
@@ -39,7 +39,12 @@ fun LatestReadItem(
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(path = coverPath, modifier = Modifier.size(48.dp, 64.dp)) CoilBookCover(
name = record.bookName,
author = record.bookAuthor,
path = coverPath,
modifier = Modifier.size(48.dp, 64.dp)
)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
AppText( AppText(
@@ -89,7 +94,12 @@ fun TimelineSessionItem(
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(path = coverPath, modifier = Modifier.size(40.dp, 56.dp)) CoilBookCover(
name = session.bookName,
author = session.bookAuthor,
path = coverPath,
modifier = Modifier.size(40.dp, 56.dp)
)
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
AppText( AppText(
@@ -131,7 +141,12 @@ fun ReadRecordItem(
.padding(16.dp), .padding(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Cover(path = coverPath, modifier = Modifier.size(40.dp, 56.dp)) CoilBookCover(
name = detail.bookName,
author = detail.bookAuthor,
path = coverPath,
modifier = Modifier.size(40.dp, 56.dp)
)
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
AppText( AppText(
@@ -12,7 +12,7 @@ import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.cover.CoilBookCover
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -107,11 +107,13 @@ fun BookStackView(
coverPath = viewModel.getBookCover(name, author) coverPath = viewModel.getBookCover(name, author)
} }
Cover( CoilBookCover(
name = name,
author = author,
path = coverPath, path = coverPath,
modifier = Modifier modifier = Modifier
.padding(end = (index * 12).dp) .padding(end = (index * 12).dp)
.size(32.dp, 44.dp) .width(32.dp)
) )
} }
} }
@@ -13,6 +13,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
@@ -78,6 +79,7 @@ fun CoilBookCover(
Box( Box(
modifier = modifier modifier = modifier
.aspectRatio(5f / 7f)
.then( .then(
if (CoverConfig.coverShowShadow) { if (CoverConfig.coverShowShadow) {
Modifier.shadow(4.dp, RoundedCornerShape(4.dp)) Modifier.shadow(4.dp, RoundedCornerShape(4.dp))