[优化] 增加书籍详情界面的阅读历史
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.data.entities.readRecord
|
||||
|
||||
data class ReadRecordTimelineDay(
|
||||
val date: String,
|
||||
val sessions: List<ReadRecordSession>
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import android.net.Uri
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||
|
||||
data class BookInfoUiState(
|
||||
val book: Book? = null,
|
||||
@@ -12,6 +13,8 @@ data class BookInfoUiState(
|
||||
val kindLabels: List<String> = emptyList(),
|
||||
val groupNames: String? = null,
|
||||
val hasCustomGroup: Boolean = false,
|
||||
val readRecordTotalTime: Long = 0L,
|
||||
val readRecordTimelineDays: List<ReadRecordTimelineDay> = emptyList(),
|
||||
val inBookshelf: Boolean = false,
|
||||
val bookSource: BookSource? = null,
|
||||
val isTocLoading: Boolean = true,
|
||||
@@ -26,6 +29,7 @@ sealed interface BookInfoSheet {
|
||||
data object CoverPicker : BookInfoSheet
|
||||
data object GroupPicker : BookInfoSheet
|
||||
data object SourcePicker : BookInfoSheet
|
||||
data object ReadRecord : BookInfoSheet
|
||||
data class WebFiles(val openAfterImport: Boolean) : BookInfoSheet
|
||||
data class ArchiveEntries(
|
||||
val archiveUri: Uri,
|
||||
@@ -68,6 +72,7 @@ sealed interface BookInfoIntent {
|
||||
data object CoverLongClick : BookInfoIntent
|
||||
data object GroupClick : BookInfoIntent
|
||||
data object ChangeSourceClick : BookInfoIntent
|
||||
data object ReadRecordClick : BookInfoIntent
|
||||
data object RemarkClick : BookInfoIntent
|
||||
data object ConfirmBackAddToShelf : BookInfoIntent
|
||||
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
|
||||
@@ -141,6 +146,7 @@ enum class BookInfoMenuAction {
|
||||
Upload,
|
||||
SyncRemote,
|
||||
Refresh,
|
||||
ReadRecord,
|
||||
Login,
|
||||
Top,
|
||||
SetSourceVariable,
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package io.legado.app.ui.book.info
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Timeline
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.hutool.core.date.DateUtil
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
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.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.formatReadDuration
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun BookReadRecordSheet(
|
||||
show: Boolean,
|
||||
totalReadTime: Long,
|
||||
timelineDays: List<ReadRecordTimelineDay>,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.read_record),
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Timeline,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
Column {
|
||||
AppText(
|
||||
text = stringResource(R.string.all_read_time),
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
color = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
AppText(
|
||||
text = formatReadDuration(totalReadTime),
|
||||
style = LegadoTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (timelineDays.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessage(message = stringResource(R.string.empty))
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
timelineDays.forEach { day ->
|
||||
item(key = "header_${day.date}") {
|
||||
AppText(
|
||||
text = day.date,
|
||||
style = LegadoTheme.typography.titleSmall,
|
||||
color = LegadoTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
items(day.sessions, key = { it.id }) { session ->
|
||||
TimelineSessionRow(session = session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimelineSessionRow(session: ReadRecordSession) {
|
||||
val lineColor = LegadoTheme.colorScheme.surfaceContainerHigh
|
||||
val nodeColor = LegadoTheme.colorScheme.primary
|
||||
val duration = (session.endTime - session.startTime).coerceAtLeast(0L)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
val x = 12.dp.toPx()
|
||||
val centerY = size.height / 2f
|
||||
drawLine(
|
||||
color = lineColor,
|
||||
start = Offset(x, 0f),
|
||||
end = Offset(x, size.height),
|
||||
strokeWidth = 2.dp.toPx()
|
||||
)
|
||||
drawCircle(
|
||||
color = nodeColor,
|
||||
radius = 4.dp.toPx(),
|
||||
center = Offset(x, centerY)
|
||||
)
|
||||
}
|
||||
.padding(start = 28.dp, top = 6.dp, bottom = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
AppText(
|
||||
text = DateUtil.format(Date(session.endTime), "HH:mm"),
|
||||
style = LegadoTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
AppText(
|
||||
text = formatReadDuration(duration),
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (session.words > 0) {
|
||||
TextCard(
|
||||
text = "第${session.words}章",
|
||||
textStyle = LegadoTheme.typography.labelSmall,
|
||||
backgroundColor = LegadoTheme.colorScheme.secondaryContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Book
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
import androidx.compose.material.icons.filled.BookmarkAdd
|
||||
import androidx.compose.material.icons.filled.Code
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
@@ -41,9 +40,8 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Timeline
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.BookmarkBorder
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.FolderZip
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
@@ -265,12 +263,12 @@ private fun BookInfoScreenContent(
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
BookInfoActions(
|
||||
hasCustomGroup = state.hasCustomGroup,
|
||||
inBookshelf = state.inBookshelf,
|
||||
onShelfClick = { onIntent(BookInfoIntent.ShelfClick) },
|
||||
onTocClick = { onIntent(BookInfoIntent.TocClick) },
|
||||
onGroupClick = { onIntent(BookInfoIntent.GroupClick) },
|
||||
onSourceClick = { onIntent(BookInfoIntent.ChangeSourceClick) },
|
||||
onReadRecordClick = { onIntent(BookInfoIntent.ReadRecordClick) },
|
||||
)
|
||||
BookInfoSummary(
|
||||
book = book,
|
||||
@@ -325,6 +323,12 @@ private fun BookInfoScreenContent(
|
||||
},
|
||||
)
|
||||
}
|
||||
BookInfoSheet.ReadRecord -> BookReadRecordSheet(
|
||||
show = currentSheet == BookInfoSheet.ReadRecord,
|
||||
totalReadTime = state.readRecordTotalTime,
|
||||
timelineDays = state.readRecordTimelineDays,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissSheet) },
|
||||
)
|
||||
is BookInfoSheet.WebFiles -> WebFileSheet(
|
||||
show = currentSheet is BookInfoSheet.WebFiles,
|
||||
files = state.webFiles,
|
||||
@@ -551,6 +555,10 @@ private fun BookInfoOverflowMenu(
|
||||
text = stringResource(R.string.refresh),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.Refresh) }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.read_record),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.ReadRecord) }
|
||||
)
|
||||
if (book?.isLocal == true) {
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.re_sync_webdav),
|
||||
@@ -736,31 +744,38 @@ private fun BookInfoHeader(
|
||||
|
||||
@Composable
|
||||
private fun BookInfoActions(
|
||||
hasCustomGroup: Boolean,
|
||||
inBookshelf: Boolean,
|
||||
onShelfClick: () -> Unit,
|
||||
onTocClick: () -> Unit,
|
||||
onGroupClick: () -> Unit,
|
||||
onSourceClick: () -> Unit,
|
||||
onReadRecordClick: () -> Unit,
|
||||
) {
|
||||
var awaitingShelfAddition by rememberSaveable { mutableStateOf(false) }
|
||||
var showShelfRemoveHint by rememberSaveable { mutableStateOf(false) }
|
||||
var showLongPressGroupHint by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(inBookshelf, awaitingShelfAddition) {
|
||||
LaunchedEffect(inBookshelf) {
|
||||
if (awaitingShelfAddition && inBookshelf) {
|
||||
awaitingShelfAddition = false
|
||||
showShelfRemoveHint = true
|
||||
delay(1000)
|
||||
showShelfRemoveHint = false
|
||||
showLongPressGroupHint = true
|
||||
delay(1000)
|
||||
showLongPressGroupHint = false
|
||||
} else if (!inBookshelf) {
|
||||
awaitingShelfAddition = false
|
||||
showShelfRemoveHint = false
|
||||
showLongPressGroupHint = false
|
||||
}
|
||||
}
|
||||
|
||||
val shelfLabel = when {
|
||||
!inBookshelf -> stringResource(R.string.add_to_bookshelf)
|
||||
showShelfRemoveHint -> stringResource(R.string.click_to_remove)
|
||||
else -> stringResource(R.string.remove_from_bookshelf)
|
||||
showLongPressGroupHint -> stringResource(R.string.long_press_group)
|
||||
inBookshelf -> stringResource(R.string.already_in_bookshelf)
|
||||
else -> stringResource(R.string.add_to_bookshelf)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -774,12 +789,14 @@ private fun BookInfoActions(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = if (inBookshelf) Icons.Outlined.Book else Icons.Default.BookmarkAdd,
|
||||
label = shelfLabel,
|
||||
onLongClick = onGroupClick,
|
||||
onClick = {
|
||||
if (!inBookshelf) {
|
||||
awaitingShelfAddition = true
|
||||
} else {
|
||||
awaitingShelfAddition = false
|
||||
showShelfRemoveHint = false
|
||||
showLongPressGroupHint = false
|
||||
}
|
||||
onShelfClick()
|
||||
},
|
||||
@@ -790,18 +807,18 @@ private fun BookInfoActions(
|
||||
label = stringResource(R.string.view_toc),
|
||||
onClick = onTocClick
|
||||
)
|
||||
BookInfoActionCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = if (hasCustomGroup) Icons.Default.Bookmark else Icons.Outlined.BookmarkBorder,
|
||||
label = stringResource(R.string.change_group),
|
||||
onClick = onGroupClick
|
||||
)
|
||||
BookInfoActionCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = Icons.Default.Code,
|
||||
label = stringResource(R.string.book_source),
|
||||
onClick = onSourceClick
|
||||
)
|
||||
BookInfoActionCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = Icons.Default.Timeline,
|
||||
label = stringResource(R.string.read_record),
|
||||
onClick = onReadRecordClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,12 +827,14 @@ private fun BookInfoActionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = modifier,
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.2f),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
) {
|
||||
Column(
|
||||
|
||||
@@ -398,8 +398,8 @@ fun ChangeSourceSheet(
|
||||
}.collectAsStateWithLifecycle()
|
||||
SelectionItemCard(
|
||||
title = item.originName,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.2f),
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
selectedContainerColor = LegadoTheme.colorScheme.primaryContainer.copy(alpha = 0.32f),
|
||||
leadingContent = {
|
||||
MediumIconButton(
|
||||
onClick = {
|
||||
@@ -503,15 +503,35 @@ fun ChangeSourceSheet(
|
||||
})
|
||||
}
|
||||
|
||||
if (mismatchBook != null) {
|
||||
AppAlertDialog(show = true, onDismissRequest = { mismatchBook = null }, title = stringResource(R.string.book_type_different), text = stringResource(R.string.soure_change_source), confirmText = stringResource(android.R.string.ok), onConfirm = { actionBook = mismatchBook; mismatchBook = null }, dismissText = stringResource(android.R.string.cancel), onDismiss = { mismatchBook = null })
|
||||
}
|
||||
actionBook?.let { searchBook ->
|
||||
AppAlertDialog(show = true, onDismissRequest = { actionBook = null }, title = stringResource(R.string.change_source_option_title), dismissText = stringResource(R.string.add_as_new_book), onDismiss = { performAction(searchBook, false) }, confirmText = stringResource(R.string.replace_current_book), onConfirm = { performAction(searchBook, true) })
|
||||
}
|
||||
if (loadingAction) {
|
||||
AppAlertDialog(show = true, onDismissRequest = {}, content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
|
||||
})
|
||||
}
|
||||
AppAlertDialog(
|
||||
data = mismatchBook,
|
||||
onDismissRequest = { mismatchBook = null },
|
||||
title = stringResource(R.string.book_type_different),
|
||||
text = stringResource(R.string.soure_change_source),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { searchBook ->
|
||||
actionBook = searchBook
|
||||
mismatchBook = null
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { mismatchBook = null }
|
||||
)
|
||||
AppAlertDialog(
|
||||
data = actionBook,
|
||||
onDismissRequest = { actionBook = null },
|
||||
title = stringResource(R.string.change_source_option_title),
|
||||
dismissText = stringResource(R.string.add_as_new_book),
|
||||
onDismiss = { actionBook?.let { performAction(it, false) } },
|
||||
confirmText = stringResource(R.string.replace_current_book),
|
||||
onConfirm = { performAction(it, true) }
|
||||
)
|
||||
AppAlertDialog(
|
||||
show = loadingAction,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||
import io.legado.app.data.repository.ReadRecordRepository
|
||||
import io.legado.app.data.repository.RemoteBookRepository
|
||||
import io.legado.app.exception.NoBooksDirException
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -48,15 +50,20 @@ import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BookInfoViewModel(
|
||||
application: Application,
|
||||
private val remoteBookRepository: RemoteBookRepository
|
||||
private val remoteBookRepository: RemoteBookRepository,
|
||||
private val readRecordRepository: ReadRecordRepository
|
||||
) : BaseViewModel(application) {
|
||||
|
||||
private val _uiState = MutableStateFlow(BookInfoUiState())
|
||||
@@ -66,11 +73,18 @@ class BookInfoViewModel(
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
private var currentBook: Book? = null
|
||||
set(value) {
|
||||
field = value
|
||||
observeReadRecordIfNeeded(value)
|
||||
}
|
||||
private var currentChapterList: List<BookChapter> = emptyList()
|
||||
private var currentWebFiles: List<BookInfoWebFile> = emptyList()
|
||||
private var currentKindLabels: List<String> = emptyList()
|
||||
private var currentGroupNames: String? = null
|
||||
private var currentHasCustomGroup = false
|
||||
private var currentReadRecordTotalTime = 0L
|
||||
private var currentReadRecordTimelineDays: List<ReadRecordTimelineDay> = emptyList()
|
||||
private var observingReadRecordKey: String? = null
|
||||
private var chapterChanged = false
|
||||
|
||||
var inBookshelf = false
|
||||
@@ -79,6 +93,7 @@ class BookInfoViewModel(
|
||||
private set
|
||||
|
||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||
private var readRecordObserveJob: Job? = null
|
||||
|
||||
fun initData(intent: Intent) {
|
||||
if (currentBook != null) return
|
||||
@@ -123,6 +138,7 @@ class BookInfoViewModel(
|
||||
|
||||
BookInfoIntent.GroupClick -> setSheet(BookInfoSheet.GroupPicker)
|
||||
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
||||
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
||||
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
||||
BookInfoIntent.ConfirmBackAddToShelf -> {
|
||||
dismissDialog()
|
||||
@@ -924,6 +940,7 @@ class BookInfoViewModel(
|
||||
}
|
||||
BookInfoMenuAction.SyncRemote -> syncFromRemote()
|
||||
BookInfoMenuAction.Refresh -> refreshCurrentBook()
|
||||
BookInfoMenuAction.ReadRecord -> setSheet(BookInfoSheet.ReadRecord)
|
||||
BookInfoMenuAction.Login -> bookSource?.let {
|
||||
emitEffect(BookInfoEffect.OpenSourceLogin(it.bookSourceUrl))
|
||||
}
|
||||
@@ -1081,6 +1098,42 @@ class BookInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeReadRecordIfNeeded(book: Book?) {
|
||||
if (book == null) {
|
||||
clearReadRecordObserve()
|
||||
return
|
||||
}
|
||||
val key = "${book.name}|||${book.author}"
|
||||
if (observingReadRecordKey == key && readRecordObserveJob?.isActive == true) return
|
||||
observingReadRecordKey = key
|
||||
readRecordObserveJob?.cancel()
|
||||
readRecordObserveJob = viewModelScope.launch {
|
||||
combine(
|
||||
readRecordRepository.getBookReadTime(book.name, book.author),
|
||||
readRecordRepository.getBookTimelineDays(book.name, book.author)
|
||||
) { totalTime, timelineDays ->
|
||||
totalTime to timelineDays
|
||||
}.collectLatest { (totalTime, timelineDays) ->
|
||||
currentReadRecordTotalTime = totalTime
|
||||
currentReadRecordTimelineDays = timelineDays
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
readRecordTotalTime = currentReadRecordTotalTime,
|
||||
readRecordTimelineDays = currentReadRecordTimelineDays
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearReadRecordObserve() {
|
||||
readRecordObserveJob?.cancel()
|
||||
readRecordObserveJob = null
|
||||
observingReadRecordKey = null
|
||||
currentReadRecordTotalTime = 0L
|
||||
currentReadRecordTimelineDays = emptyList()
|
||||
}
|
||||
|
||||
private fun dismissSheet() {
|
||||
setSheet(BookInfoSheet.None)
|
||||
}
|
||||
@@ -1110,6 +1163,8 @@ class BookInfoViewModel(
|
||||
kindLabels = currentKindLabels,
|
||||
groupNames = currentGroupNames,
|
||||
hasCustomGroup = currentHasCustomGroup,
|
||||
readRecordTotalTime = currentReadRecordTotalTime,
|
||||
readRecordTimelineDays = currentReadRecordTimelineDays,
|
||||
inBookshelf = inBookshelf,
|
||||
bookSource = bookSource,
|
||||
isTocLoading = isTocLoading,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
fun formatReadDuration(millis: Long): String {
|
||||
val days = millis / (1000 * 60 * 60 * 24)
|
||||
val hours = millis % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)
|
||||
val minutes = millis % (1000 * 60 * 60) / (1000 * 60)
|
||||
val seconds = millis % (1000 * 60) / 1000
|
||||
val d = if (days > 0) "${days}天" else ""
|
||||
val h = if (hours > 0) "${hours}小时" else ""
|
||||
val m = if (minutes > 0) "${minutes}分钟" else ""
|
||||
val s = if (seconds > 0) "${seconds}秒" else ""
|
||||
return if ("$d$h$m$s".isBlank()) "0秒" else "$d$h$m$s"
|
||||
}
|
||||
Reference in New Issue
Block a user