界面优化

This commit is contained in:
HapeLee
2026-05-16 04:30:30 +08:00
parent 59afc9dd94
commit 8539cf818a
21 changed files with 798 additions and 396 deletions
@@ -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()
@@ -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