界面优化
This commit is contained in:
@@ -5,12 +5,17 @@ import io.legado.app.data.dao.BookChapterDao
|
||||
import io.legado.app.data.dao.BookDao
|
||||
import io.legado.app.data.entities.Book
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class BookRepository(
|
||||
private val bookDao: BookDao,
|
||||
private val bookChapterDao: BookChapterDao
|
||||
) {
|
||||
fun getAllBooks(): Flow<List<Book>> {
|
||||
return bookDao.flowAll()
|
||||
}
|
||||
|
||||
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
|
||||
|
||||
+17
-2
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.domain.usecase.readRecord
|
||||
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||
import io.legado.app.ui.book.readRecord.ReadBookRanking
|
||||
@@ -16,7 +17,8 @@ class GetReadRecordOverviewUseCase {
|
||||
period: ReadPeriod,
|
||||
refDate: LocalDate,
|
||||
details: List<ReadRecordDetail>,
|
||||
latestRecords: List<ReadRecord>
|
||||
latestRecords: List<ReadRecord>,
|
||||
allBooks: List<Book>
|
||||
): ReadRecordOverviewUiState {
|
||||
val (startDate, endDate) = getPeriodRange(period, refDate)
|
||||
|
||||
@@ -41,11 +43,24 @@ class GetReadRecordOverviewUseCase {
|
||||
val totalBooks = periodBooks.size
|
||||
|
||||
val shelfBooksMap = latestRecords.associateBy { it.bookName to it.bookAuthor }
|
||||
val allShelfBooksMap = allBooks.associateBy { it.name to it.author }
|
||||
|
||||
var readingCount = 0
|
||||
var finishedCount = 0
|
||||
|
||||
periodBooks.keys.forEach { key ->
|
||||
if (shelfBooksMap.containsKey(key)) {
|
||||
readingCount++
|
||||
}
|
||||
// 判断是否读完
|
||||
allShelfBooksMap[key]?.let { book ->
|
||||
if (book.totalChapterNum > 0 &&
|
||||
book.durChapterIndex >= book.totalChapterNum - 1 &&
|
||||
book.durChapterPos != 0
|
||||
) {
|
||||
finishedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val topBooks = periodBooks.map { (key, details) ->
|
||||
@@ -122,7 +137,7 @@ class GetReadRecordOverviewUseCase {
|
||||
totalTime = totalTime,
|
||||
readingDays = readingDays,
|
||||
totalBooks = totalBooks,
|
||||
finishedBooks = 0, // Placeholder
|
||||
finishedBooks = finishedCount,
|
||||
readingBooks = readingCount,
|
||||
totalWords = totalWords,
|
||||
dailyTimeData = dailyTimeData,
|
||||
|
||||
@@ -75,6 +75,7 @@ sealed interface BookInfoIntent {
|
||||
data object ChangeSourceClick : BookInfoIntent
|
||||
data object ReadRecordClick : BookInfoIntent
|
||||
data object RemarkClick : BookInfoIntent
|
||||
data class SaveCover(val path: String) : BookInfoIntent
|
||||
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
|
||||
data class UpdateRemark(val remark: String) : BookInfoIntent
|
||||
data class SelectGroup(val groupId: Long) : BookInfoIntent
|
||||
|
||||
@@ -82,6 +82,7 @@ import io.legado.app.ui.theme.LocalHazeState
|
||||
import io.legado.app.ui.theme.ProvideThemeOverride
|
||||
import io.legado.app.ui.theme.ThemeOverrideState
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.fadingEdge
|
||||
import io.legado.app.ui.theme.rememberImageSeedColor
|
||||
import io.legado.app.ui.theme.rememberThemeOverride
|
||||
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
|
||||
@@ -737,8 +738,12 @@ private fun BookInfoHeader(
|
||||
}
|
||||
}
|
||||
if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) {
|
||||
val kindListState = rememberLazyListState()
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = kindListState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fadingEdge(kindListState),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
groupNames?.takeIf { it.isNotBlank() }?.let {
|
||||
@@ -963,101 +968,106 @@ private fun BookInfoDialogs(
|
||||
var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
|
||||
var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
|
||||
|
||||
if (dialog is BookInfoDialog.DeleteBook) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.draw),
|
||||
text = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = {
|
||||
if (dialog.isLocal) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
androidx.compose.material3.Checkbox(
|
||||
checked = deleteOriginal,
|
||||
onCheckedChange = { deleteOriginal = it },
|
||||
colors = androidx.compose.material3.CheckboxDefaults.colors(
|
||||
checkedColor = LegadoTheme.colorScheme.primary,
|
||||
checkmarkColor = LegadoTheme.colorScheme.onPrimary,
|
||||
uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
AppAlertDialog(
|
||||
data = dialog as? BookInfoDialog.DeleteBook,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.draw),
|
||||
text = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = { d ->
|
||||
if (d.isLocal) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
androidx.compose.material3.Checkbox(
|
||||
checked = deleteOriginal,
|
||||
onCheckedChange = { deleteOriginal = it },
|
||||
colors = androidx.compose.material3.CheckboxDefaults.colors(
|
||||
checkedColor = LegadoTheme.colorScheme.primary,
|
||||
checkmarkColor = LegadoTheme.colorScheme.onPrimary,
|
||||
uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(text = stringResource(R.string.delete_book_file))
|
||||
}
|
||||
)
|
||||
Text(text = stringResource(R.string.delete_book_file))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (dialog is BookInfoDialog.EditRemark) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.edit_remark),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = {
|
||||
AppTextField(
|
||||
value = remarkText,
|
||||
onValueChange = { remarkText = it },
|
||||
label = "备注",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
AppAlertDialog(
|
||||
data = dialog as? BookInfoDialog.EditRemark,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.edit_remark),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = {
|
||||
AppTextField(
|
||||
value = remarkText,
|
||||
onValueChange = { remarkText = it },
|
||||
label = stringResource(R.string.book_remark),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (dialog is BookInfoDialog.UnsupportedWebFile) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.draw),
|
||||
text = stringResource(R.string.file_not_supported, dialog.webFile.name),
|
||||
confirmText = stringResource(R.string.open_fun),
|
||||
onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(dialog.webFile)) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
)
|
||||
}
|
||||
val unsupportedWebFile = dialog as? BookInfoDialog.UnsupportedWebFile
|
||||
AppAlertDialog(
|
||||
data = unsupportedWebFile,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.draw),
|
||||
text = unsupportedWebFile?.let {
|
||||
stringResource(
|
||||
R.string.file_not_supported,
|
||||
it.webFile.name
|
||||
)
|
||||
},
|
||||
confirmText = stringResource(R.string.open_fun),
|
||||
onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(it.webFile)) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
)
|
||||
|
||||
if (dialog is BookInfoDialog.PhotoPreview) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.img_cover),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = {
|
||||
AsyncImage(
|
||||
model = dialog.path,
|
||||
contentDescription = null,
|
||||
AppAlertDialog(
|
||||
data = dialog as? BookInfoDialog.PhotoPreview,
|
||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
title = stringResource(R.string.img_cover),
|
||||
confirmText = "保存到相册",
|
||||
onConfirm = { d ->
|
||||
onIntent(BookInfoIntent.SaveCover(d.path))
|
||||
onIntent(BookInfoIntent.DismissDialog)
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||
content = { d ->
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
CoilBookCover(
|
||||
name = state.book?.name,
|
||||
author = state.book?.author,
|
||||
path = d.path,
|
||||
sourceOrigin = state.book?.origin,
|
||||
ignoreUseDefaultCover = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 420.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
.heightIn(max = 420.dp)
|
||||
.fillMaxWidth(0.6f)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (state.isBusy) {
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
AppCircularProgressIndicator()
|
||||
}
|
||||
AppAlertDialog(
|
||||
show = state.isBusy,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
AppCircularProgressIndicator()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
AppLogSheet(show = state.showAppLogSheet, onDismissRequest = { onIntent(BookInfoIntent.DismissAppLogSheet) })
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package io.legado.app.ui.book.info
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.imageLoader
|
||||
import coil.request.SuccessResult
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
@@ -16,11 +20,11 @@ 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.domain.usecase.ChangeBookSourceUseCase
|
||||
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||
import io.legado.app.data.repository.BookGroupRepository
|
||||
import io.legado.app.data.repository.ReadRecordRepository
|
||||
import io.legado.app.data.repository.RemoteBookRepository
|
||||
import io.legado.app.data.repository.BookGroupRepository
|
||||
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||
import io.legado.app.exception.NoBooksDirException
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -33,7 +37,6 @@ import io.legado.app.help.book.isSameNameAuthor
|
||||
import io.legado.app.help.book.isWebFile
|
||||
import io.legado.app.help.book.removeType
|
||||
import io.legado.app.help.book.updateTo
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.lib.webdav.ObjectNotFoundException
|
||||
@@ -45,24 +48,29 @@ import io.legado.app.model.SourceCallBack
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.ImageSaveUtils
|
||||
import io.legado.app.utils.UrlUtil
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
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.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class BookInfoViewModel(
|
||||
application: Application,
|
||||
@@ -167,6 +175,9 @@ class BookInfoViewModel(
|
||||
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
||||
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
||||
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
||||
is BookInfoIntent.SaveCover -> {
|
||||
saveCoverToGallery(intent.path)
|
||||
}
|
||||
is BookInfoIntent.ConfirmDelete -> {
|
||||
dismissDialog()
|
||||
deleteBook(intent.deleteOriginal)
|
||||
@@ -445,6 +456,41 @@ class BookInfoViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveCoverToGallery(path: String) {
|
||||
val book = currentBook
|
||||
val sourceOrigin = if (book?.getDisplayCover() == path) book.origin else null
|
||||
execute {
|
||||
setBusy(true)
|
||||
val request = buildCoverImageRequest(
|
||||
context = context,
|
||||
data = path,
|
||||
sourceOrigin = sourceOrigin,
|
||||
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
||||
crossfade = false
|
||||
)
|
||||
val result = context.imageLoader.execute(request)
|
||||
if (result is SuccessResult) {
|
||||
val bitmap = result.drawable.toBitmap()
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
val byteArray = outputStream.toByteArray()
|
||||
ImageSaveUtils.saveImageToGallery(context, byteArray, "Cover_")
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}.onSuccess { success ->
|
||||
if (success) {
|
||||
context.toastOnUi("保存成功")
|
||||
} else {
|
||||
context.toastOnUi("保存失败")
|
||||
}
|
||||
}.onFinally {
|
||||
setBusy(false)
|
||||
}.onError {
|
||||
context.toastOnUi("保存出错: ${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
fun saveRemark(remark: String, success: (() -> Unit)? = null) {
|
||||
currentBook?.let { book ->
|
||||
execute {
|
||||
@@ -713,27 +759,22 @@ class BookInfoViewModel(
|
||||
|
||||
private fun refreshMeta(book: Book) {
|
||||
execute {
|
||||
val allKinds = book.getKindList()
|
||||
val customKinds = allKinds.filter { it.startsWith("#") }
|
||||
val sourceKinds = allKinds.filter { !it.startsWith("#") }.toMutableList()
|
||||
val fileSizePattern = Regex("""^\d[\d,.]*\s*(b|kb|M|G|T)$""", RegexOption.IGNORE_CASE)
|
||||
val kinds = (book.kind?.splitNotBlank(",", "\n").orEmpty().toList())
|
||||
.filter { !fileSizePattern.matches(it.trim()) }
|
||||
.toMutableList()
|
||||
if (book.isLocal) {
|
||||
val size = FileDoc.fromFile(book.bookUrl).size
|
||||
if (size > 0) {
|
||||
sourceKinds.add(ConvertUtils.formatFileSize(size))
|
||||
kinds.add(ConvertUtils.formatFileSize(size))
|
||||
}
|
||||
}
|
||||
val mergedKinds = (customKinds + sourceKinds).distinct()
|
||||
val finalKinds = kinds.distinct()
|
||||
val userGroupIds = appDb.bookGroupDao.idsSum
|
||||
val groupAnd = userGroupIds and book.group
|
||||
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
||||
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
||||
val normalizedGroupNames = groupNames.ifBlank { null }
|
||||
val persistedCustomKinds = currentKindLabels.filter { it.startsWith("#") }
|
||||
val finalKinds = if (persistedCustomKinds.isNotEmpty()) {
|
||||
(persistedCustomKinds + sourceKinds).distinct()
|
||||
} else {
|
||||
mergedKinds
|
||||
}
|
||||
book.kind = finalKinds.joinToString(",")
|
||||
appDb.bookDao.update(book)
|
||||
Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.legado.app.ui.book.info.edit
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
@@ -13,14 +13,16 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.filled.ImageSearch
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
@@ -30,8 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -41,27 +42,37 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.fadingEdge
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||
import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.utils.SelectImageContract
|
||||
import io.legado.app.utils.launch
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -214,6 +225,7 @@ fun BookInfoEditContent(
|
||||
KindEditor(
|
||||
kindList = uiState.kindList,
|
||||
onKindListChange = { viewModel.onKindListChange(it) },
|
||||
onReset = { viewModel.resetKinds() },
|
||||
backgroundColor = inputBackgroundColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -298,89 +310,146 @@ fun BookTypeDropdown(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun KindEditor(
|
||||
kindList: List<String>,
|
||||
onKindListChange: (List<String>) -> Unit,
|
||||
onReset: () -> Unit,
|
||||
backgroundColor: Color
|
||||
) {
|
||||
var newKindText by remember { mutableStateOf("") }
|
||||
var editingIndex by remember { mutableStateOf<Int?>(null) }
|
||||
var editText by remember { mutableStateOf("") }
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
val listState = rememberLazyListState()
|
||||
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||
val mutable = kindList.toMutableList()
|
||||
mutable.add(to.index, mutable.removeAt(from.index))
|
||||
onKindListChange(mutable)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
LazyRow(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fadingEdge(listState),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppTextField(
|
||||
value = newKindText,
|
||||
onValueChange = { newKindText = it },
|
||||
label = "标签",
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
MediumOutlinedIconButton(
|
||||
onClick = {
|
||||
val trimmed = newKindText.trim()
|
||||
val tag = if (trimmed.startsWith("#")) trimmed else "#$trimmed"
|
||||
if (tag.isNotBlank() && tag !in kindList) {
|
||||
val sortedList = (kindList + tag).sortedWith(
|
||||
compareBy<String> { !it.startsWith("#") }.thenBy { it }
|
||||
)
|
||||
onKindListChange(sortedList)
|
||||
newKindText = ""
|
||||
}
|
||||
},
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
if (kindList.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
kindList.forEach { kind ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppTextField(
|
||||
value = kind,
|
||||
onValueChange = { newValue ->
|
||||
val index = kindList.indexOf(kind)
|
||||
if (index >= 0) {
|
||||
val newList = kindList.toMutableList()
|
||||
newList[index] = newValue
|
||||
onKindListChange(newList)
|
||||
items(kindList.size, key = { kindList[it] }) { index ->
|
||||
val kind = kindList[index]
|
||||
ReorderableItem(reorderableState, key = kind) { isDragging ->
|
||||
KindChip(
|
||||
text = kind,
|
||||
isDragging = isDragging,
|
||||
onClick = {
|
||||
editingIndex = index
|
||||
editText = kind
|
||||
},
|
||||
modifier = Modifier
|
||||
.longPressDraggableHandle(
|
||||
onDragStarted = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||
},
|
||||
onDragStopped = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||
}
|
||||
},
|
||||
label = null,
|
||||
backgroundColor = backgroundColor,
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.width(80.dp)
|
||||
)
|
||||
IconButton(
|
||||
onClick = {
|
||||
onKindListChange(kindList - kind)
|
||||
},
|
||||
modifier = Modifier.padding(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "删除标签",
|
||||
tint = Color.Red.copy(alpha = 0.7f),
|
||||
modifier = Modifier.padding(4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
.animateItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
MediumOutlinedIconButton(
|
||||
onClick = {
|
||||
onReset()
|
||||
},
|
||||
imageVector = Icons.Default.Replay
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
MediumOutlinedIconButton(
|
||||
onClick = {
|
||||
editingIndex = -1
|
||||
editText = ""
|
||||
},
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
|
||||
if (editingIndex != null) {
|
||||
val isAdding = editingIndex == -1
|
||||
AppAlertDialog(
|
||||
show = true,
|
||||
onDismissRequest = { editingIndex = null },
|
||||
title = if (isAdding) "添加标签" else "编辑标签",
|
||||
content = {
|
||||
AppTextField(
|
||||
value = editText,
|
||||
onValueChange = { editText = it },
|
||||
label = "标签",
|
||||
backgroundColor = backgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
val trimmed = editText.trim()
|
||||
if (trimmed.isNotBlank()) {
|
||||
val mutable = kindList.toMutableList()
|
||||
if (isAdding) {
|
||||
if (trimmed !in kindList) mutable.add(trimmed)
|
||||
} else {
|
||||
mutable[editingIndex!!] = trimmed
|
||||
}
|
||||
onKindListChange(mutable)
|
||||
}
|
||||
editingIndex = null
|
||||
},
|
||||
dismissText = if (isAdding) stringResource(android.R.string.cancel) else stringResource(
|
||||
R.string.delete
|
||||
),
|
||||
onDismiss = if (isAdding) {
|
||||
{ editingIndex = null }
|
||||
} else {
|
||||
{
|
||||
onKindListChange(kindList.toMutableList().apply { removeAt(editingIndex!!) })
|
||||
editingIndex = null
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun KindChip(
|
||||
text: String,
|
||||
isDragging: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = LegadoTheme.colorScheme.surfaceContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
shadowElevation = if (isDragging) 8.dp else 0.dp,
|
||||
modifier = modifier
|
||||
.zIndex(if (isDragging) 1f else 0f)
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
AnimatedTextLine(
|
||||
text = text,
|
||||
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||
color = LegadoTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import io.legado.app.model.ReadBook
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.MD5Utils
|
||||
import io.legado.app.utils.inputStream
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -33,6 +32,7 @@ data class BookInfoEditUiState(
|
||||
val intro: String? = null,
|
||||
val remark: String? = null,
|
||||
val kindList: List<String> = emptyList(),
|
||||
val originalKindList: List<String> = emptyList(),
|
||||
val selectedType: String = "文本",
|
||||
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
||||
val fixedType: Boolean = false,
|
||||
@@ -53,13 +53,16 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
it.isAudio -> 1
|
||||
else -> 0
|
||||
}
|
||||
val kinds =
|
||||
it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty()
|
||||
_uiState.value = BookInfoEditUiState(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
coverUrl = it.getDisplayCover(),
|
||||
intro = it.getDisplayIntro(),
|
||||
remark = it.remark,
|
||||
kindList = it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty(),
|
||||
kindList = kinds,
|
||||
originalKindList = kinds,
|
||||
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
||||
fixedType = it.config.fixedType,
|
||||
book = it
|
||||
@@ -68,6 +71,10 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
}
|
||||
}
|
||||
|
||||
fun resetKinds() {
|
||||
_uiState.value = _uiState.value.copy(kindList = _uiState.value.originalKindList.toList())
|
||||
}
|
||||
|
||||
fun onNameChange(name: String) {
|
||||
_uiState.value = _uiState.value.copy(name = name)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
package io.legado.app.ui.book.manage
|
||||
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -26,11 +23,11 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Bookmarks
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
@@ -44,7 +41,6 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FloatingActionButtonMenu
|
||||
import androidx.compose.material3.FloatingActionButtonMenuItem
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
@@ -61,9 +57,10 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -72,7 +69,6 @@ import io.legado.app.R
|
||||
import io.legado.app.constant.IntentAction
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
||||
@@ -87,12 +83,12 @@ import io.legado.app.ui.book.info.ChangeSourceSheet
|
||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
@@ -100,26 +96,25 @@ import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
|
||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||
import io.legado.app.utils.ACache
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.checkWrite
|
||||
import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.move
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startService
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.verificationField
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||
@@ -399,14 +394,14 @@ private fun BookshelfManageScreen(
|
||||
Icons.Default.SelectAll,
|
||||
stringResource(R.string.select_all)
|
||||
) {
|
||||
selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl }
|
||||
selectedBookUrls = filteredBooks.mapTo(hashSetOf()) { it.bookUrl }
|
||||
},
|
||||
BookshelfManageFabAction(
|
||||
Icons.Default.Refresh,
|
||||
stringResource(R.string.revert_selection)
|
||||
) {
|
||||
val visibleBookUrls = booksByUrl.keys
|
||||
selectedBookUrls = visibleBookUrls - selectedBookUrls
|
||||
val filteredUrls = filteredBooks.map { it.bookUrl }.toSet()
|
||||
selectedBookUrls = (selectedBookUrls - filteredUrls) + (filteredUrls - selectedBookUrls)
|
||||
},
|
||||
BookshelfManageFabAction(
|
||||
Icons.Default.Download,
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
package io.legado.app.ui.book.readRecord
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.SizeTransform
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowRight
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material.icons.filled.CalendarMonth
|
||||
import androidx.compose.material.icons.filled.GridOn
|
||||
import androidx.compose.material.icons.filled.Leaderboard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
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.TextAlign
|
||||
@@ -31,6 +58,7 @@ 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.cover.CoilBookCover
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapMode
|
||||
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||
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.GlassTopAppBarDefaults
|
||||
@@ -83,8 +111,11 @@ fun ReadRecordOverviewScreen(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
item {
|
||||
HeatmapCard(state)
|
||||
|
||||
if (state.period != ReadPeriod.ALL && state.dailyTimeData.isNotEmpty()) {
|
||||
item {
|
||||
ReadingTimeBarChartCard(data = state.dailyTimeData, period = state.period)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
@@ -99,10 +130,8 @@ fun ReadRecordOverviewScreen(
|
||||
StatsGridCard(title = "阅读数据", items = stats)
|
||||
}
|
||||
|
||||
if (state.period != ReadPeriod.ALL && state.dailyTimeData.isNotEmpty()) {
|
||||
item {
|
||||
ReadingTimeBarChartCard(data = state.dailyTimeData, period = state.period)
|
||||
}
|
||||
item {
|
||||
HeatmapCard(state)
|
||||
}
|
||||
|
||||
if (state.topBooks.isNotEmpty()) {
|
||||
@@ -124,27 +153,22 @@ fun PeriodSelector(
|
||||
selectedPeriod: ReadPeriod,
|
||||
onPeriodSelected: (ReadPeriod) -> Unit
|
||||
) {
|
||||
val periods = listOf(
|
||||
ReadPeriod.DAY to "日",
|
||||
ReadPeriod.WEEK to "周",
|
||||
ReadPeriod.MONTH to "月",
|
||||
ReadPeriod.YEAR to "年",
|
||||
ReadPeriod.ALL to "总"
|
||||
)
|
||||
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = periods.indexOfFirst { it.first == selectedPeriod },
|
||||
containerColor = Color.Transparent,
|
||||
divider = {}
|
||||
) {
|
||||
periods.forEach { (period, label) ->
|
||||
Tab(
|
||||
selected = selectedPeriod == period,
|
||||
onClick = { onPeriodSelected(period) },
|
||||
text = { AppText(label) }
|
||||
)
|
||||
}
|
||||
val periods = remember {
|
||||
listOf(
|
||||
ReadPeriod.DAY to "日",
|
||||
ReadPeriod.WEEK to "周",
|
||||
ReadPeriod.MONTH to "月",
|
||||
ReadPeriod.YEAR to "年",
|
||||
ReadPeriod.ALL to "总"
|
||||
)
|
||||
}
|
||||
|
||||
AppTabRow(
|
||||
tabTitles = periods.map { it.second },
|
||||
selectedTabIndex = periods.indexOfFirst { it.first == selectedPeriod },
|
||||
onTabSelected = { index -> onPeriodSelected(periods[index].first) },
|
||||
isScrollable = false
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -156,34 +180,50 @@ fun DateNavigator(
|
||||
) {
|
||||
if (period == ReadPeriod.ALL) return
|
||||
|
||||
val dateText = when (period) {
|
||||
ReadPeriod.DAY -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年M月d日"))
|
||||
ReadPeriod.WEEK -> {
|
||||
val start = referenceDate.with(java.time.DayOfWeek.MONDAY)
|
||||
val end = referenceDate.with(java.time.DayOfWeek.SUNDAY)
|
||||
"${start.format(DateTimeFormatter.ofPattern("M.d"))} - ${end.format(DateTimeFormatter.ofPattern("M.d"))}"
|
||||
}
|
||||
ReadPeriod.MONTH -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年M月"))
|
||||
ReadPeriod.YEAR -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年"))
|
||||
ReadPeriod.ALL -> ""
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MediumIconButton(
|
||||
onClick = onPrevClick,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowLeft
|
||||
)
|
||||
AppText(
|
||||
text = dateText,
|
||||
style = LegadoTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = referenceDate,
|
||||
transitionSpec = {
|
||||
if (targetState.isAfter(initialState)) {
|
||||
(slideInHorizontally { it / 2 } + fadeIn()).togetherWith(slideOutHorizontally { -it / 2 } + fadeOut())
|
||||
} else {
|
||||
(slideInHorizontally { -it / 2 } + fadeIn()).togetherWith(slideOutHorizontally { it / 2 } + fadeOut())
|
||||
}.using(SizeTransform(clip = false))
|
||||
},
|
||||
label = "DateNavigator"
|
||||
) { targetDate ->
|
||||
val text = when (period) {
|
||||
ReadPeriod.DAY -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年M月d日"))
|
||||
ReadPeriod.WEEK -> {
|
||||
val start = targetDate.with(java.time.DayOfWeek.MONDAY)
|
||||
val end = targetDate.with(java.time.DayOfWeek.SUNDAY)
|
||||
"${start.format(DateTimeFormatter.ofPattern("M.d"))} - ${
|
||||
end.format(
|
||||
DateTimeFormatter.ofPattern("M.d")
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
ReadPeriod.MONTH -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年M月"))
|
||||
ReadPeriod.YEAR -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年"))
|
||||
ReadPeriod.ALL -> ""
|
||||
}
|
||||
AppText(
|
||||
text = text,
|
||||
style = LegadoTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
MediumIconButton(
|
||||
onClick = onNextClick,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowRight
|
||||
@@ -199,7 +239,16 @@ fun HeatmapCard(state: ReadRecordOverviewUiState) {
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
AppText("阅读热力图", style = LegadoTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.GridOn,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText("阅读热力图", style = LegadoTheme.typography.titleMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
HeatmapCalendarSection(
|
||||
dailyReadCounts = state.allReadCounts,
|
||||
@@ -224,11 +273,22 @@ fun TopReadingListCard(
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||
AppText(
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = "阅读时长榜",
|
||||
style = LegadoTheme.typography.titleMedium
|
||||
)
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Leaderboard,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText(
|
||||
text = "阅读时长榜",
|
||||
style = LegadoTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
topBooks.forEachIndexed { index, book ->
|
||||
@@ -247,7 +307,7 @@ fun TopReadingListCard(
|
||||
AppText(
|
||||
text = "${index + 1}",
|
||||
style = LegadoTheme.typography.titleMedium,
|
||||
modifier = Modifier.width(32.dp),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
@@ -259,6 +319,12 @@ fun TopReadingListCard(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
AppText(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
text = ReadRecordFormatter.formatDuration(book.readTime),
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
color = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
AppText(
|
||||
text = book.bookName,
|
||||
style = LegadoTheme.typography.bodyMedium,
|
||||
@@ -272,12 +338,6 @@ fun TopReadingListCard(
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
AppText(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
text = ReadRecordFormatter.formatDuration(book.readTime),
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
color = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,7 +360,16 @@ fun ReadingCalendarCard(
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
AppText("读书日历", style = LegadoTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarMonth,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText("读书日历", style = LegadoTheme.typography.titleMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
@@ -52,9 +52,10 @@ class ReadRecordOverviewViewModel(
|
||||
_period,
|
||||
_referenceDate,
|
||||
repository.getAllRecordDetails(""),
|
||||
repository.getLatestReadRecords("")
|
||||
) { period, refDate, details, latestRecords ->
|
||||
getReadRecordOverviewUseCase(period, refDate, details, latestRecords)
|
||||
repository.getLatestReadRecords(""),
|
||||
bookRepository.getAllBooks()
|
||||
) { period, refDate, details, latestRecords, allBooks ->
|
||||
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
|
||||
@@ -2,8 +2,6 @@ package io.legado.app.ui.book.readRecord
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -49,14 +47,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -69,17 +62,16 @@ import cn.hutool.core.date.DateUtil
|
||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||
import io.legado.app.ui.book.readRecord.component.*
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.theme.fadingEdge
|
||||
import io.legado.app.ui.widget.CollapsibleHeader
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SearchBar
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AppIconButton
|
||||
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.TextCard
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||
@@ -104,8 +96,9 @@ import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
|
||||
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.GlassTopAppBarDefaults
|
||||
import io.legado.app.utils.formatReadDuration
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||
import io.legado.app.utils.StringUtils.formatFriendlyDate
|
||||
import io.legado.app.utils.formatReadDuration
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import java.time.LocalDate
|
||||
@@ -516,29 +509,6 @@ fun HeatmapCalendarSection(
|
||||
}
|
||||
}
|
||||
|
||||
val showLeftGradient by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
||||
}
|
||||
}
|
||||
val showRightGradient by remember {
|
||||
derivedStateOf {
|
||||
listState.canScrollForward
|
||||
}
|
||||
}
|
||||
|
||||
val leftAlpha by animateFloatAsState(
|
||||
targetValue = if (showLeftGradient) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "LeftGradientAlpha"
|
||||
)
|
||||
|
||||
val rightAlpha by animateFloatAsState(
|
||||
targetValue = if (showRightGradient) 1f else 0f,
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "RightGradientAlpha"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -556,31 +526,7 @@ fun HeatmapCalendarSection(
|
||||
horizontalArrangement = Arrangement.spacedBy(config.cellSpacing),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
val width = size.width
|
||||
val gradientWidthPx = config.gradientWidth.toPx()
|
||||
val leftStop = gradientWidthPx / width
|
||||
val rightStop = 1f - (gradientWidthPx / width)
|
||||
|
||||
val colorStops = arrayOf(
|
||||
0f to Color.Black.copy(alpha = 1f - leftAlpha),
|
||||
leftStop to Color.Black,
|
||||
rightStop to Color.Black,
|
||||
1f to Color.Black.copy(alpha = 1f - rightAlpha)
|
||||
)
|
||||
|
||||
drawRect(
|
||||
brush = Brush.horizontalGradient(
|
||||
colorStops = colorStops,
|
||||
startX = 0f,
|
||||
endX = width
|
||||
),
|
||||
blendMode = BlendMode.DstIn
|
||||
)
|
||||
}
|
||||
.fadingEdge(listState, config.gradientWidth)
|
||||
) {
|
||||
val firstReadDate = listOfNotNull(
|
||||
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
package io.legado.app.ui.book.readRecord.component
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.legado.app.ui.book.readRecord.ReadPeriod
|
||||
import io.legado.app.ui.book.readRecord.ReadRecordFormatter
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
@@ -22,7 +42,16 @@ fun ReadingTimeBarChartCard(
|
||||
period: ReadPeriod,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val maxTime = data.maxOfOrNull { it.second }?.coerceAtLeast(1L) ?: 1L
|
||||
val rawMaxTime = data.maxOfOrNull { it.second }?.coerceAtLeast(1L) ?: 1L
|
||||
|
||||
// 向上取整逻辑:根据时长跨度选择合适的对齐单位
|
||||
val roundedMaxTime = when {
|
||||
rawMaxTime < 60_000 -> 60_000L // 不足1分钟取1分钟
|
||||
rawMaxTime < 10 * 60_000 -> ((rawMaxTime + 59_999) / 60_000) * 60_000L // 10分钟内按1分钟对齐
|
||||
rawMaxTime < 60 * 60_000 -> ((rawMaxTime + 5 * 60_000 - 1) / (5 * 60_000)) * 5 * 60_000L // 1小时内按5分钟对齐
|
||||
rawMaxTime < 12 * 3600_000 -> ((rawMaxTime + 3600_000 - 1) / 3600_000) * 3600_000L // 12小时内按1小时对齐
|
||||
else -> ((rawMaxTime + 4 * 3600_000 - 1) / (4 * 3600_000)) * 4 * 3600_000L // 超过12小时按4小时对齐
|
||||
}
|
||||
|
||||
GlassCard(
|
||||
modifier = modifier
|
||||
@@ -30,7 +59,16 @@ fun ReadingTimeBarChartCard(
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
AppText("阅读时长分布", style = LegadoTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BarChart,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText("阅读时长分布", style = LegadoTheme.typography.titleMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
@@ -41,13 +79,14 @@ fun ReadingTimeBarChartCard(
|
||||
// Y-Axis
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(32.dp)
|
||||
.fillMaxHeight()
|
||||
.padding(bottom = 20.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
AppText(
|
||||
text = formatChartDuration(maxTime),
|
||||
text = ReadRecordFormatter.formatDuration(roundedMaxTime),
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
fontSize = 8.sp,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
@@ -71,7 +110,12 @@ fun ReadingTimeBarChartCard(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
data.forEachIndexed { index, (date, time) ->
|
||||
val heightFactor = time.toFloat() / maxTime
|
||||
val targetHeightFactor = time.toFloat() / roundedMaxTime
|
||||
val heightFactor by animateFloatAsState(
|
||||
targetValue = targetHeightFactor,
|
||||
animationSpec = tween(durationMillis = 320, delayMillis = index * 20),
|
||||
label = "BarHeight"
|
||||
)
|
||||
|
||||
val showLabel = when (period) {
|
||||
ReadPeriod.DAY -> true
|
||||
@@ -98,6 +142,7 @@ fun ReadingTimeBarChartCard(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.widthIn(max = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.BottomCenter
|
||||
) {
|
||||
@@ -109,7 +154,7 @@ fun ReadingTimeBarChartCard(
|
||||
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
||||
.background(
|
||||
if (time > 0) LegadoTheme.colorScheme.primary
|
||||
else LegadoTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
else LegadoTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -120,7 +165,10 @@ fun ReadingTimeBarChartCard(
|
||||
text = labelText,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
fontSize = 8.sp,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Visible,
|
||||
modifier = Modifier.wrapContentWidth(unbounded = true)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -131,10 +179,3 @@ 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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
package io.legado.app.ui.book.readRecord.component
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Equalizer
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -10,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
|
||||
data class StatItem(val label: String, val value: String)
|
||||
@@ -26,12 +43,24 @@ fun StatsGridCard(
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
AppText(title, style = LegadoTheme.typography.titleMedium)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Equalizer,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
AppText(title, style = LegadoTheme.typography.titleMedium)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column {
|
||||
for (i in items.indices step 2) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
StatCell(items[i], Modifier.weight(1f))
|
||||
if (i + 1 < items.size) {
|
||||
StatCell(items[i + 1], Modifier.weight(1f))
|
||||
@@ -55,18 +84,26 @@ private fun StatCell(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppText(
|
||||
text = item.value,
|
||||
style = LegadoTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = LegadoTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
AppText(
|
||||
text = item.label,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = item.value,
|
||||
transitionSpec = {
|
||||
(fadeIn() + slideInVertically { it / 2 }).togetherWith(fadeOut() + slideOutVertically { -it / 2 })
|
||||
},
|
||||
label = "StatValue"
|
||||
) { targetValue ->
|
||||
AppText(
|
||||
text = targetValue,
|
||||
style = LegadoTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = LegadoTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,17 @@ object MainNavigator {
|
||||
}
|
||||
}
|
||||
|
||||
MainRouteRssFavorites,
|
||||
MainRouteRuleSub -> {
|
||||
if (currentRoute == MainRouteHome) {
|
||||
backStack.add(route)
|
||||
} else {
|
||||
backStack.clear()
|
||||
backStack.add(MainRouteHome)
|
||||
backStack.add(route)
|
||||
}
|
||||
}
|
||||
|
||||
MainRouteReadRecord -> {
|
||||
if (currentRoute == MainRouteHome) {
|
||||
backStack.add(route)
|
||||
|
||||
@@ -5,13 +5,16 @@ import androidx.compose.foundation.combinedClickable
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -51,14 +54,15 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.widget.components.SourceIcon
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.startActivity
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -136,18 +140,6 @@ fun RssScreen(
|
||||
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
||||
onSearchQueryChange = { viewModel.search(it) },
|
||||
searchPlaceholder = stringResource(R.string.search_rss_source),
|
||||
topBarActions = {
|
||||
TopBarActionButton(
|
||||
onClick = { viewModel.openRuleSub() },
|
||||
imageVector = Icons.Default.Subscriptions,
|
||||
contentDescription = stringResource(R.string.rule_subscription)
|
||||
)
|
||||
TopBarActionButton(
|
||||
onClick = { viewModel.openFavorites() },
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = stringResource(R.string.favorite)
|
||||
)
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
onClick = {
|
||||
@@ -185,6 +177,58 @@ fun RssScreen(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { viewModel.openRuleSub() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Subscriptions,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
AppText(
|
||||
text = stringResource(R.string.rule_subscription),
|
||||
style = LegadoTheme.typography.labelMediumEmphasized
|
||||
)
|
||||
}
|
||||
}
|
||||
GlassCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = { viewModel.openFavorites() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
AppText(
|
||||
text = stringResource(R.string.favorite),
|
||||
style = LegadoTheme.typography.labelMediumEmphasized
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(uiState.items, key = { it.sourceUrl }) { source ->
|
||||
RssSourceGridItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
|
||||
@@ -4,8 +4,6 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -86,11 +84,7 @@ fun RuleSubScreen(
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.sort),
|
||||
leadingIcon = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.size(12.dp))
|
||||
AppText(stringResource(R.string.sort))
|
||||
}
|
||||
Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
|
||||
},
|
||||
onClick = {
|
||||
viewModel.resetOrder()
|
||||
@@ -231,12 +225,14 @@ fun RuleSubEditDialog(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = stringResource(R.string.name),
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
AppTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
label = "URL",
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.legado.app.ui.theme
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.CompositingStrategy
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Applies a horizontal fading edge effect to a scrollable container.
|
||||
*
|
||||
* @param leftAlpha 0f = fully faded, 1f = fully visible
|
||||
* @param rightAlpha 0f = fully faded, 1f = fully visible
|
||||
* @param gradientWidth width of the fade gradient on each side
|
||||
*/
|
||||
fun Modifier.fadingEdge(
|
||||
leftAlpha: Float,
|
||||
rightAlpha: Float,
|
||||
gradientWidth: Dp = 24.dp
|
||||
): Modifier = graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
val width = size.width
|
||||
val gradientWidthPx = gradientWidth.toPx()
|
||||
val leftStop = gradientWidthPx / width
|
||||
val rightStop = 1f - (gradientWidthPx / width)
|
||||
drawRect(
|
||||
brush = Brush.horizontalGradient(
|
||||
colorStops = arrayOf(
|
||||
0f to Color.Black.copy(alpha = 1f - leftAlpha),
|
||||
leftStop to Color.Black,
|
||||
rightStop to Color.Black,
|
||||
1f to Color.Black.copy(alpha = 1f - rightAlpha)
|
||||
),
|
||||
startX = 0f,
|
||||
endX = width
|
||||
),
|
||||
blendMode = BlendMode.DstIn
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience overload that derives fade alphas from a [LazyListState].
|
||||
* Left fade appears when scrolled past the start; right fade appears when more content is available.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.fadingEdge(
|
||||
listState: LazyListState,
|
||||
gradientWidth: Dp = 24.dp
|
||||
): Modifier {
|
||||
val showLeft by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
||||
}
|
||||
}
|
||||
val showRight by remember {
|
||||
derivedStateOf { listState.canScrollForward }
|
||||
}
|
||||
val leftAlpha by animateFloatAsState(
|
||||
targetValue = if (showLeft) 1f else 0f,
|
||||
animationSpec = tween(200),
|
||||
label = "LeftFadeAlpha"
|
||||
)
|
||||
val rightAlpha by animateFloatAsState(
|
||||
targetValue = if (showRight) 1f else 0f,
|
||||
animationSpec = tween(200),
|
||||
label = "RightFadeAlpha"
|
||||
)
|
||||
return fadingEdge(leftAlpha, rightAlpha, gradientWidth)
|
||||
}
|
||||
@@ -93,7 +93,8 @@ fun MediumOutlinedIconButton(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
contentDescription = contentDescription,
|
||||
tint = LegadoTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package io.legado.app.ui.widget.components.tabRow
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -12,6 +11,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import top.yukonga.miuix.kmp.basic.TabRowDefaults
|
||||
import top.yukonga.miuix.kmp.basic.TabRowWithContour
|
||||
|
||||
@@ -20,7 +20,8 @@ fun AppTabRow(
|
||||
tabTitles: List<String>,
|
||||
selectedTabIndex: Int,
|
||||
onTabSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
isScrollable: Boolean = true
|
||||
) {
|
||||
val composeEngine = LegadoTheme.composeEngine
|
||||
|
||||
@@ -36,29 +37,59 @@ fun AppTabRow(
|
||||
)
|
||||
)
|
||||
} else {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
edgePadding = 0.dp,
|
||||
divider = { },
|
||||
containerColor = Color.Transparent,
|
||||
minTabWidth = 0.dp,
|
||||
modifier = modifier
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { onTabSelected(index) },
|
||||
text = {
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
if (isScrollable) {
|
||||
PrimaryScrollableTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
edgePadding = 0.dp,
|
||||
divider = { },
|
||||
containerColor = Color.Transparent,
|
||||
minTabWidth = 0.dp,
|
||||
modifier = modifier
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, title ->
|
||||
AppTab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { onTabSelected(index) },
|
||||
title = title
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedTabIndex,
|
||||
divider = { },
|
||||
containerColor = Color.Transparent,
|
||||
modifier = modifier
|
||||
) {
|
||||
tabTitles.forEachIndexed { index, title ->
|
||||
AppTab(
|
||||
selected = selectedTabIndex == index,
|
||||
onClick = { onTabSelected(index) },
|
||||
title = title
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppTab(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
title: String
|
||||
) {
|
||||
Tab(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
text = {
|
||||
AppText(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
color = if (selected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -90,7 +90,7 @@ object ConvertUtils {
|
||||
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
|
||||
val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt()
|
||||
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
|
||||
return DecimalFormat("#,##0.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
|
||||
return DecimalFormat("#.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
|
||||
Reference in New Issue
Block a user