优化阅读记录界面

This commit is contained in:
HapeLee
2025-11-26 02:29:07 +08:00
parent d29c889de3
commit 3dfc1f5c25
@@ -2,6 +2,8 @@ package io.legado.app.ui.book.readRecord
import android.os.Bundle
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -36,10 +38,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import cn.hutool.core.date.DateUtil
import io.legado.app.base.BaseComposeActivity
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordSession
import io.legado.app.ui.widget.compose.AnimatedTextLine
import io.legado.app.ui.widget.compose.EmptyMessageView
import io.legado.app.utils.StringUtils.formatFriendlyDate
import org.koin.androidx.compose.koinViewModel
@@ -108,8 +113,25 @@ fun ReadRecordScreen(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
Column {
LargeTopAppBar(
title = { Text("阅读记录") },
MediumTopAppBar(
title = {
val (mainTitle, subTitle) = when (displayMode) {
DisplayMode.AGGREGATE -> "阅读记录" to "汇总视图"
DisplayMode.TIMELINE -> "阅读记录" to "时间线视图"
DisplayMode.LATEST -> "阅读记录" to "最后阅读"
}
Column {
Text(
text = mainTitle,
style = MaterialTheme.typography.titleLarge,
)
AnimatedTextLine(
text = subTitle,
style = MaterialTheme.typography.titleMedium
)
}
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
@@ -151,66 +173,87 @@ fun ReadRecordScreen(
Column(modifier = Modifier.padding(padding).fillMaxSize()) {
TotalTimeHeader(state.totalReadTime)
val isEmpty = when (displayMode) {
DisplayMode.AGGREGATE -> state.groupedRecords.isEmpty()
DisplayMode.TIMELINE -> state.timelineRecords.isEmpty()
DisplayMode.LATEST -> state.latestRecords.isEmpty()
}
Crossfade(
targetState = isEmpty,
animationSpec = tween(durationMillis = 500),
label = "ContentCrossfade"
) { isListEmpty ->
if (isListEmpty){
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
EmptyMessageView(
message = "没有记录"
)
}
} else {
LazyColumn {
when(displayMode){
DisplayMode.AGGREGATE -> {
state.groupedRecords.forEach { (date, details) ->
LazyColumn {
when(displayMode){
DisplayMode.AGGREGATE -> {
state.groupedRecords.forEach { (date, details) ->
val dailyTotalTime = details.sumOf { it.readTime }
val dailyTotalTime = details.sumOf { it.readTime }
stickyHeader {
DateHeader(date, dailyTotalTime)
}
stickyHeader {
DateHeader(date, dailyTotalTime)
items(
items = details,
key = { it.bookName + it.readTime.toString() }
) { detail ->
ReadRecordItem(
detail = detail,
viewModel = viewModel,
onClick = { onBookClick(detail.bookName) },
onDelete = { viewModel.deleteDetail(detail) },
modifier = Modifier.animateItem()
)
}
}
}
DisplayMode.TIMELINE -> {
state.timelineRecords.forEach { (date, sessions) ->
val dailyTotalTime = sessions.sumOf { it.endTime - it.startTime }
stickyHeader { DateHeader(date, dailyTotalTime) }
items(
items = details,
key = { it.bookName + it.readTime.toString() }
) { detail ->
ReadRecordItem(
detail = detail,
val timelineItems = sessions.mapIndexed { index, session ->
val previousSession = sessions.getOrNull(index - 1)
val showHeader = index == 0 || session.bookName != previousSession?.bookName
TimelineItem(session, showHeader)
}
items(items = timelineItems, key = { it.session.id }) { item ->
TimelineSessionItem(
item = item,
onBookClick = onBookClick,
viewModel = viewModel
)
}
}
}
DisplayMode.LATEST -> {
items(items = state.latestRecords, key = { it.bookName + it.deviceId }) { record ->
LatestReadItem(
record = record,
viewModel = viewModel,
onClick = { onBookClick(detail.bookName) },
onDelete = { viewModel.deleteDetail(detail) },
onClick = { onBookClick(record.bookName) },
modifier = Modifier.animateItem()
)
}
}
}
DisplayMode.TIMELINE -> {
state.timelineRecords.forEach { (date, sessions) ->
val dailyTotalTime = sessions.sumOf { it.endTime - it.startTime }
stickyHeader { DateHeader(date, dailyTotalTime) }
val timelineItems = sessions.mapIndexed { index, session ->
val previousSession = sessions.getOrNull(index - 1)
val showHeader = index == 0 || session.bookName != previousSession?.bookName
TimelineItem(session, showHeader)
}
items(items = timelineItems, key = { it.session.id }) { item ->
TimelineSessionItem(
item = item,
onBookClick = onBookClick,
viewModel = viewModel
)
}
}
}
DisplayMode.LATEST -> {
items(items = state.latestRecords, key = { it.bookName + it.deviceId }) { record ->
LatestReadItem(
record = record,
viewModel = viewModel,
onClick = { onBookClick(record.bookName) },
modifier = Modifier.animateItem()
)
}
}
}
}
}
}
}
}
@@ -232,7 +275,7 @@ fun LatestReadItem(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
BookCoverWithPlaceholder(coverPath)
@@ -344,11 +387,11 @@ fun TimelineSessionItem(
"时长: ${formatDuring(duration)}",
style = MaterialTheme.typography.bodySmall
)
// Text(
// "字数: ${session.words}",
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
// Text(
// "字数: ${session.words}",
// style = MaterialTheme.typography.bodySmall,
// color = MaterialTheme.colorScheme.onSurfaceVariant
// )
}
}
}
@@ -372,7 +415,7 @@ fun ReadRecordItem(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
BookCoverWithPlaceholder(coverPath)
@@ -431,9 +474,8 @@ fun DateHeader(
dailyTotalTime: Long
) {
val dateText = formatFriendlyDate(date)
val totalTimeText = "阅读时长: ${formatDuring(dailyTotalTime)}"
val totalTimeText = "已读 ${formatDuring(dailyTotalTime)}"
Surface(
color = MaterialTheme.colorScheme.surface,
tonalElevation = 2.dp,
modifier = Modifier.fillMaxWidth()
) {
@@ -446,8 +488,6 @@ fun DateHeader(
color = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = totalTimeText,
style = MaterialTheme.typography.bodySmall,
@@ -459,22 +499,36 @@ fun DateHeader(
@Composable
fun TotalTimeHeader(time: Long) {
Column(
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer
),
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f))
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Text("总阅读时长", style = MaterialTheme.typography.labelMedium)
Text(
text = formatDuring(time),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold
)
Column(
modifier = Modifier
.padding(12.dp),
horizontalAlignment = Alignment.Start
) {
Text(
text = "阅读时长",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = formatDuring(time),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBarSection(