|
|
@@ -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)
|
|
|
-
|
|
|
- LazyColumn {
|
|
|
- when(displayMode){
|
|
|
- DisplayMode.AGGREGATE -> {
|
|
|
- state.groupedRecords.forEach { (date, details) ->
|
|
|
-
|
|
|
- val dailyTotalTime = details.sumOf { it.readTime }
|
|
|
-
|
|
|
- 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()
|
|
|
- )
|
|
|
+ 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) ->
|
|
|
+
|
|
|
+ val dailyTotalTime = details.sumOf { it.readTime }
|
|
|
+
|
|
|
+ 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) }
|
|
|
-
|
|
|
- val timelineItems = sessions.mapIndexed { index, session ->
|
|
|
- val previousSession = sessions.getOrNull(index - 1)
|
|
|
-
|
|
|
- val showHeader = index == 0 || session.bookName != previousSession?.bookName
|
|
|
- TimelineItem(session, showHeader)
|
|
|
+ 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
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
-
|
|
|
- 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()
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- 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(
|