界面优化

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
+2 -1
View File
@@ -82,7 +82,8 @@
"Bash(findstr /i \"miuix.*\\\\.kt\")", "Bash(findstr /i \"miuix.*\\\\.kt\")",
"Bash(findstr /v \"\\\\\\\\build\\\\\\\\\")", "Bash(findstr /v \"\\\\\\\\build\\\\\\\\\")",
"Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")", "Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")",
"Skill(legado-compose-review)" "Skill(legado-compose-review)",
"Bash(cmd /c \".\\\\gradlew.bat :app:compileAppDebugKotlin\")"
] ]
} }
} }
@@ -5,12 +5,17 @@ import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao import io.legado.app.data.dao.BookDao
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class BookRepository( class BookRepository(
private val bookDao: BookDao, private val bookDao: BookDao,
private val bookChapterDao: BookChapterDao private val bookChapterDao: BookChapterDao
) { ) {
fun getAllBooks(): Flow<List<Book>> {
return bookDao.flowAll()
}
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? { suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
bookDao.getBook(bookName, bookAuthor)?.getDisplayCover() bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
@@ -1,5 +1,6 @@
package io.legado.app.domain.usecase.readRecord 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.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.ui.book.readRecord.ReadBookRanking import io.legado.app.ui.book.readRecord.ReadBookRanking
@@ -16,7 +17,8 @@ class GetReadRecordOverviewUseCase {
period: ReadPeriod, period: ReadPeriod,
refDate: LocalDate, refDate: LocalDate,
details: List<ReadRecordDetail>, details: List<ReadRecordDetail>,
latestRecords: List<ReadRecord> latestRecords: List<ReadRecord>,
allBooks: List<Book>
): ReadRecordOverviewUiState { ): ReadRecordOverviewUiState {
val (startDate, endDate) = getPeriodRange(period, refDate) val (startDate, endDate) = getPeriodRange(period, refDate)
@@ -41,11 +43,24 @@ class GetReadRecordOverviewUseCase {
val totalBooks = periodBooks.size val totalBooks = periodBooks.size
val shelfBooksMap = latestRecords.associateBy { it.bookName to it.bookAuthor } val shelfBooksMap = latestRecords.associateBy { it.bookName to it.bookAuthor }
val allShelfBooksMap = allBooks.associateBy { it.name to it.author }
var readingCount = 0 var readingCount = 0
var finishedCount = 0
periodBooks.keys.forEach { key -> periodBooks.keys.forEach { key ->
if (shelfBooksMap.containsKey(key)) { if (shelfBooksMap.containsKey(key)) {
readingCount++ readingCount++
} }
// 判断是否读完
allShelfBooksMap[key]?.let { book ->
if (book.totalChapterNum > 0 &&
book.durChapterIndex >= book.totalChapterNum - 1 &&
book.durChapterPos != 0
) {
finishedCount++
}
}
} }
val topBooks = periodBooks.map { (key, details) -> val topBooks = periodBooks.map { (key, details) ->
@@ -122,7 +137,7 @@ class GetReadRecordOverviewUseCase {
totalTime = totalTime, totalTime = totalTime,
readingDays = readingDays, readingDays = readingDays,
totalBooks = totalBooks, totalBooks = totalBooks,
finishedBooks = 0, // Placeholder finishedBooks = finishedCount,
readingBooks = readingCount, readingBooks = readingCount,
totalWords = totalWords, totalWords = totalWords,
dailyTimeData = dailyTimeData, dailyTimeData = dailyTimeData,
@@ -75,6 +75,7 @@ sealed interface BookInfoIntent {
data object ChangeSourceClick : BookInfoIntent data object ChangeSourceClick : BookInfoIntent
data object ReadRecordClick : BookInfoIntent data object ReadRecordClick : BookInfoIntent
data object RemarkClick : BookInfoIntent data object RemarkClick : BookInfoIntent
data class SaveCover(val path: String) : BookInfoIntent
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
data class UpdateRemark(val remark: String) : BookInfoIntent data class UpdateRemark(val remark: String) : BookInfoIntent
data class SelectGroup(val groupId: Long) : 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.ProvideThemeOverride
import io.legado.app.ui.theme.ThemeOverrideState import io.legado.app.ui.theme.ThemeOverrideState
import io.legado.app.ui.theme.ThemeResolver 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.rememberImageSeedColor
import io.legado.app.ui.theme.rememberThemeOverride import io.legado.app.ui.theme.rememberThemeOverride
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
@@ -737,8 +738,12 @@ private fun BookInfoHeader(
} }
} }
if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) { if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) {
val kindListState = rememberLazyListState()
LazyRow( LazyRow(
modifier = Modifier.fillMaxWidth(), state = kindListState,
modifier = Modifier
.fillMaxWidth()
.fadingEdge(kindListState),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
groupNames?.takeIf { it.isNotBlank() }?.let { groupNames?.takeIf { it.isNotBlank() }?.let {
@@ -963,101 +968,106 @@ private fun BookInfoDialogs(
var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) } var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) } var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
if (dialog is BookInfoDialog.DeleteBook) { AppAlertDialog(
AppAlertDialog( data = dialog as? BookInfoDialog.DeleteBook,
show = true, onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) }, title = stringResource(R.string.draw),
title = stringResource(R.string.draw), text = stringResource(R.string.sure_del),
text = stringResource(R.string.sure_del), confirmText = stringResource(android.R.string.ok),
confirmText = stringResource(android.R.string.ok), onConfirm = {
onConfirm = { onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal)) },
}, dismissText = stringResource(android.R.string.cancel),
dismissText = stringResource(android.R.string.cancel), onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
onDismiss = { onIntent(BookInfoIntent.DismissDialog) }, content = { d ->
content = { if (d.isLocal) {
if (dialog.isLocal) { Row(verticalAlignment = Alignment.CenterVertically) {
Row(verticalAlignment = Alignment.CenterVertically) { androidx.compose.material3.Checkbox(
androidx.compose.material3.Checkbox( checked = deleteOriginal,
checked = deleteOriginal, onCheckedChange = { deleteOriginal = it },
onCheckedChange = { deleteOriginal = it }, colors = androidx.compose.material3.CheckboxDefaults.colors(
colors = androidx.compose.material3.CheckboxDefaults.colors( checkedColor = LegadoTheme.colorScheme.primary,
checkedColor = LegadoTheme.colorScheme.primary, checkmarkColor = LegadoTheme.colorScheme.onPrimary,
checkmarkColor = LegadoTheme.colorScheme.onPrimary, uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
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(
AppAlertDialog( data = dialog as? BookInfoDialog.EditRemark,
show = true, onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) }, title = stringResource(R.string.edit_remark),
title = stringResource(R.string.edit_remark), confirmText = stringResource(android.R.string.ok),
confirmText = stringResource(android.R.string.ok), onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) }, dismissText = stringResource(android.R.string.cancel),
dismissText = stringResource(android.R.string.cancel), onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
onDismiss = { onIntent(BookInfoIntent.DismissDialog) }, content = {
content = { AppTextField(
AppTextField( value = remarkText,
value = remarkText, onValueChange = { remarkText = it },
onValueChange = { remarkText = it }, label = stringResource(R.string.book_remark),
label = "备注", modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(), )
) }
} )
)
}
if (dialog is BookInfoDialog.UnsupportedWebFile) { val unsupportedWebFile = dialog as? BookInfoDialog.UnsupportedWebFile
AppAlertDialog( AppAlertDialog(
show = true, data = unsupportedWebFile,
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) }, onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
title = stringResource(R.string.draw), title = stringResource(R.string.draw),
text = stringResource(R.string.file_not_supported, dialog.webFile.name), text = unsupportedWebFile?.let {
confirmText = stringResource(R.string.open_fun), stringResource(
onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(dialog.webFile)) }, R.string.file_not_supported,
dismissText = stringResource(android.R.string.cancel), it.webFile.name
onDismiss = { onIntent(BookInfoIntent.DismissDialog) }, )
) },
} 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(
AppAlertDialog( data = dialog as? BookInfoDialog.PhotoPreview,
show = true, onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) }, title = stringResource(R.string.img_cover),
title = stringResource(R.string.img_cover), confirmText = "保存到相册",
confirmText = stringResource(android.R.string.ok), onConfirm = { d ->
onConfirm = { onIntent(BookInfoIntent.DismissDialog) }, onIntent(BookInfoIntent.SaveCover(d.path))
content = { onIntent(BookInfoIntent.DismissDialog)
AsyncImage( },
model = dialog.path, dismissText = stringResource(android.R.string.cancel),
contentDescription = null, 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 modifier = Modifier
.fillMaxWidth() .heightIn(max = 420.dp)
.heightIn(max = 420.dp), .fillMaxWidth(0.6f)
contentScale = ContentScale.Fit,
) )
} }
) }
} )
if (state.isBusy) { AppAlertDialog(
AppAlertDialog( show = state.isBusy,
show = true, onDismissRequest = {},
onDismissRequest = {}, content = {
content = { Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { AppCircularProgressIndicator()
AppCircularProgressIndicator()
}
} }
) }
} )
AppLogSheet(show = state.showAppLogSheet, onDismissRequest = { onIntent(BookInfoIntent.DismissAppLogSheet) }) 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.Activity.RESULT_OK
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import coil.imageLoader
import coil.request.SuccessResult
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog 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.BookChapter
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
import io.legado.app.domain.usecase.ChangeBookSourceUseCase import io.legado.app.data.repository.BookGroupRepository
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
import io.legado.app.data.repository.ReadRecordRepository import io.legado.app.data.repository.ReadRecordRepository
import io.legado.app.data.repository.RemoteBookRepository 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.domain.usecase.ClearBookCacheUseCase
import io.legado.app.exception.NoBooksDirException import io.legado.app.exception.NoBooksDirException
import io.legado.app.exception.NoStackTraceException 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.isWebFile
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.help.book.updateTo 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.config.LocalConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.webdav.ObjectNotFoundException 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.analyzeRule.AnalyzeUrl
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.webBook.WebBook 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.ArchiveUtils
import io.legado.app.utils.ConvertUtils import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.FileDoc import io.legado.app.utils.FileDoc
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.ImageSaveUtils
import io.legado.app.utils.UrlUtil import io.legado.app.utils.UrlUtil
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
class BookInfoViewModel( class BookInfoViewModel(
application: Application, application: Application,
@@ -167,6 +175,9 @@ class BookInfoViewModel(
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker) BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord) BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark)) BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
is BookInfoIntent.SaveCover -> {
saveCoverToGallery(intent.path)
}
is BookInfoIntent.ConfirmDelete -> { is BookInfoIntent.ConfirmDelete -> {
dismissDialog() dismissDialog()
deleteBook(intent.deleteOriginal) 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) { fun saveRemark(remark: String, success: (() -> Unit)? = null) {
currentBook?.let { book -> currentBook?.let { book ->
execute { execute {
@@ -713,27 +759,22 @@ class BookInfoViewModel(
private fun refreshMeta(book: Book) { private fun refreshMeta(book: Book) {
execute { execute {
val allKinds = book.getKindList() val fileSizePattern = Regex("""^\d[\d,.]*\s*(b|kb|M|G|T)$""", RegexOption.IGNORE_CASE)
val customKinds = allKinds.filter { it.startsWith("#") } val kinds = (book.kind?.splitNotBlank(",", "\n").orEmpty().toList())
val sourceKinds = allKinds.filter { !it.startsWith("#") }.toMutableList() .filter { !fileSizePattern.matches(it.trim()) }
.toMutableList()
if (book.isLocal) { if (book.isLocal) {
val size = FileDoc.fromFile(book.bookUrl).size val size = FileDoc.fromFile(book.bookUrl).size
if (size > 0) { 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 userGroupIds = appDb.bookGroupDao.idsSum
val groupAnd = userGroupIds and book.group val groupAnd = userGroupIds and book.group
val hasCustomGroup = book.group > 0L && groupAnd != 0L val hasCustomGroup = book.group > 0L && groupAnd != 0L
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",") val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
val normalizedGroupNames = groupNames.ifBlank { null } 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(",") book.kind = finalKinds.joinToString(",")
appDb.bookDao.update(book) appDb.bookDao.update(book)
Triple(finalKinds, normalizedGroupNames, hasCustomGroup) Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
@@ -1,9 +1,9 @@
package io.legado.app.ui.book.info.edit package io.legado.app.ui.book.info.edit
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets 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.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add 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.FolderOpen
import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.icons.filled.ImageSearch
import androidx.compose.material.icons.filled.Replay 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.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Surface
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -41,27 +42,37 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.book.changecover.ChangeCoverDialog 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.AppScaffold
import io.legado.app.ui.widget.components.AppTextField 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.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.cover.CoilBookCover
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu 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.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem 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.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults 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.SelectImageContract
import io.legado.app.utils.launch import io.legado.app.utils.launch
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.showDialogFragment
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -214,6 +225,7 @@ fun BookInfoEditContent(
KindEditor( KindEditor(
kindList = uiState.kindList, kindList = uiState.kindList,
onKindListChange = { viewModel.onKindListChange(it) }, onKindListChange = { viewModel.onKindListChange(it) },
onReset = { viewModel.resetKinds() },
backgroundColor = inputBackgroundColor backgroundColor = inputBackgroundColor
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -298,89 +310,146 @@ fun BookTypeDropdown(
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun KindEditor( fun KindEditor(
kindList: List<String>, kindList: List<String>,
onKindListChange: (List<String>) -> Unit, onKindListChange: (List<String>) -> Unit,
onReset: () -> Unit,
backgroundColor: Color backgroundColor: Color
) { ) {
var newKindText by remember { mutableStateOf("") } var editingIndex by remember { mutableStateOf<Int?>(null) }
var editText by remember { mutableStateOf("") }
val hapticFeedback = LocalHapticFeedback.current
Column( val listState = rememberLazyListState()
modifier = Modifier.fillMaxWidth() 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( LazyRow(
modifier = Modifier.fillMaxWidth(), state = listState,
modifier = Modifier
.weight(1f)
.fadingEdge(listState),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
AppTextField( items(kindList.size, key = { kindList[it] }) { index ->
value = newKindText, val kind = kindList[index]
onValueChange = { newKindText = it }, ReorderableItem(reorderableState, key = kind) { isDragging ->
label = "标签", KindChip(
backgroundColor = backgroundColor, text = kind,
modifier = Modifier.weight(1f) isDragging = isDragging,
) onClick = {
Spacer(modifier = Modifier.width(8.dp)) editingIndex = index
MediumOutlinedIconButton( editText = kind
onClick = { },
val trimmed = newKindText.trim() modifier = Modifier
val tag = if (trimmed.startsWith("#")) trimmed else "#$trimmed" .longPressDraggableHandle(
if (tag.isNotBlank() && tag !in kindList) { onDragStarted = {
val sortedList = (kindList + tag).sortedWith( hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
compareBy<String> { !it.startsWith("#") }.thenBy { it } },
) onDragStopped = {
onKindListChange(sortedList) hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
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)
} }
},
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.FileUtils
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
import io.legado.app.utils.inputStream import io.legado.app.utils.inputStream
import io.legado.app.utils.splitNotBlank
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -33,6 +32,7 @@ data class BookInfoEditUiState(
val intro: String? = null, val intro: String? = null,
val remark: String? = null, val remark: String? = null,
val kindList: List<String> = emptyList(), val kindList: List<String> = emptyList(),
val originalKindList: List<String> = emptyList(),
val selectedType: String = "文本", val selectedType: String = "文本",
val bookTypes: List<String> = listOf("文本", "音频", "图片"), val bookTypes: List<String> = listOf("文本", "音频", "图片"),
val fixedType: Boolean = false, val fixedType: Boolean = false,
@@ -53,13 +53,16 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
it.isAudio -> 1 it.isAudio -> 1
else -> 0 else -> 0
} }
val kinds =
it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty()
_uiState.value = BookInfoEditUiState( _uiState.value = BookInfoEditUiState(
name = it.name, name = it.name,
author = it.author, author = it.author,
coverUrl = it.getDisplayCover(), coverUrl = it.getDisplayCover(),
intro = it.getDisplayIntro(), intro = it.getDisplayIntro(),
remark = it.remark, remark = it.remark,
kindList = it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty(), kindList = kinds,
originalKindList = kinds,
selectedType = _uiState.value.bookTypes[selectedTypeIndex], selectedType = _uiState.value.bookTypes[selectedTypeIndex],
fixedType = it.config.fixedType, fixedType = it.config.fixedType,
book = it 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) { fun onNameChange(name: String) {
_uiState.value = _uiState.value.copy(name = name) _uiState.value = _uiState.value.copy(name = name)
} }
@@ -1,23 +1,20 @@
package io.legado.app.ui.book.manage package io.legado.app.ui.book.manage
import androidx.compose.ui.focus.focusRequester
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable 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.Arrangement
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width 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.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuOpen 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.Bookmarks
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download 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.Info
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PlayArrow 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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonMenu import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem import androidx.compose.material3.FloatingActionButtonMenuItem
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -61,9 +57,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment 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.focus.focusRequester
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.constant.IntentAction
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book 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.BookSource
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem 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.book.info.GroupSelectSheet
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPadding 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.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog 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.SmallTonalIconButton
import io.legado.app.ui.widget.components.button.SmallTonalTextButton 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.NormalCard
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.card.SelectionItemCard 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.cover.CoilBookCover
import io.legado.app.ui.widget.components.divider.PillDivider 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.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.icon.AppIcons
import io.legado.app.ui.widget.components.list.ListScaffold 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.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.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.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.text.AppText
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.utils.ACache import io.legado.app.utils.ACache
import io.legado.app.utils.FileDoc import io.legado.app.utils.FileDoc
import io.legado.app.utils.checkWrite import io.legado.app.utils.checkWrite
import io.legado.app.utils.isContentScheme import io.legado.app.utils.isContentScheme
import io.legado.app.utils.move import io.legado.app.utils.move
import io.legado.app.utils.startActivity
import io.legado.app.utils.startService import io.legado.app.utils.startService
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import io.legado.app.utils.verificationField 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 org.koin.androidx.compose.koinViewModel
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -399,14 +394,14 @@ private fun BookshelfManageScreen(
Icons.Default.SelectAll, Icons.Default.SelectAll,
stringResource(R.string.select_all) stringResource(R.string.select_all)
) { ) {
selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl } selectedBookUrls = filteredBooks.mapTo(hashSetOf()) { it.bookUrl }
}, },
BookshelfManageFabAction( BookshelfManageFabAction(
Icons.Default.Refresh, Icons.Default.Refresh,
stringResource(R.string.revert_selection) stringResource(R.string.revert_selection)
) { ) {
val visibleBookUrls = booksByUrl.keys val filteredUrls = filteredBooks.map { it.bookUrl }.toSet()
selectedBookUrls = visibleBookUrls - selectedBookUrls selectedBookUrls = (selectedBookUrls - filteredUrls) + (filteredUrls - selectedBookUrls)
}, },
BookshelfManageFabAction( BookshelfManageFabAction(
Icons.Default.Download, Icons.Default.Download,
@@ -1,20 +1,47 @@
package io.legado.app.ui.book.readRecord 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.background
import androidx.compose.foundation.clickable 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.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowLeft import androidx.compose.material.icons.automirrored.filled.ArrowLeft
import androidx.compose.material.icons.automirrored.filled.ArrowRight import androidx.compose.material.icons.automirrored.filled.ArrowRight
import androidx.compose.material3.* import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.runtime.* 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign 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.card.GlassCard
import io.legado.app.ui.widget.components.cover.CoilBookCover 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.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.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -83,8 +111,11 @@ fun ReadRecordOverviewScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 16.dp) 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 { item {
@@ -99,10 +130,8 @@ fun ReadRecordOverviewScreen(
StatsGridCard(title = "阅读数据", items = stats) StatsGridCard(title = "阅读数据", items = stats)
} }
if (state.period != ReadPeriod.ALL && state.dailyTimeData.isNotEmpty()) { item {
item { HeatmapCard(state)
ReadingTimeBarChartCard(data = state.dailyTimeData, period = state.period)
}
} }
if (state.topBooks.isNotEmpty()) { if (state.topBooks.isNotEmpty()) {
@@ -124,27 +153,22 @@ fun PeriodSelector(
selectedPeriod: ReadPeriod, selectedPeriod: ReadPeriod,
onPeriodSelected: (ReadPeriod) -> Unit onPeriodSelected: (ReadPeriod) -> Unit
) { ) {
val periods = listOf( val periods = remember {
ReadPeriod.DAY to "", listOf(
ReadPeriod.WEEK to "", ReadPeriod.DAY to "",
ReadPeriod.MONTH to "", ReadPeriod.WEEK to "",
ReadPeriod.YEAR to "", ReadPeriod.MONTH to "",
ReadPeriod.ALL 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) }
)
}
} }
AppTabRow(
tabTitles = periods.map { it.second },
selectedTabIndex = periods.indexOfFirst { it.first == selectedPeriod },
onTabSelected = { index -> onPeriodSelected(periods[index].first) },
isScrollable = false
)
} }
@Composable @Composable
@@ -156,34 +180,50 @@ fun DateNavigator(
) { ) {
if (period == ReadPeriod.ALL) return 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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 8.dp, horizontal = 12.dp),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
MediumIconButton( MediumIconButton(
onClick = onPrevClick, onClick = onPrevClick,
imageVector = Icons.AutoMirrored.Filled.ArrowLeft imageVector = Icons.AutoMirrored.Filled.ArrowLeft
) )
AppText( AnimatedContent(
text = dateText, targetState = referenceDate,
style = LegadoTheme.typography.titleMedium, transitionSpec = {
modifier = Modifier.padding(horizontal = 16.dp) 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( MediumIconButton(
onClick = onNextClick, onClick = onNextClick,
imageVector = Icons.AutoMirrored.Filled.ArrowRight imageVector = Icons.AutoMirrored.Filled.ArrowRight
@@ -199,7 +239,16 @@ fun HeatmapCard(state: ReadRecordOverviewUiState) {
.adaptiveHorizontalPadding(vertical = 8.dp) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(16.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)) Spacer(modifier = Modifier.height(8.dp))
HeatmapCalendarSection( HeatmapCalendarSection(
dailyReadCounts = state.allReadCounts, dailyReadCounts = state.allReadCounts,
@@ -224,11 +273,22 @@ fun TopReadingListCard(
.adaptiveHorizontalPadding(vertical = 8.dp) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(vertical = 16.dp)) { Column(modifier = Modifier.padding(vertical = 16.dp)) {
AppText( Row(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
text = "阅读时长榜", verticalAlignment = Alignment.CenterVertically
style = LegadoTheme.typography.titleMedium ) {
) 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)) Spacer(modifier = Modifier.height(8.dp))
topBooks.forEachIndexed { index, book -> topBooks.forEachIndexed { index, book ->
@@ -247,7 +307,7 @@ fun TopReadingListCard(
AppText( AppText(
text = "${index + 1}", text = "${index + 1}",
style = LegadoTheme.typography.titleMedium, style = LegadoTheme.typography.titleMedium,
modifier = Modifier.width(32.dp), modifier = Modifier.padding(start = 12.dp, end = 16.dp),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
) )
@@ -259,6 +319,12 @@ fun TopReadingListCard(
) )
Spacer(modifier = Modifier.width(12.dp)) Spacer(modifier = Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { 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( AppText(
text = book.bookName, text = book.bookName,
style = LegadoTheme.typography.bodyMedium, style = LegadoTheme.typography.bodyMedium,
@@ -272,12 +338,6 @@ fun TopReadingListCard(
color = LegadoTheme.colorScheme.onSurfaceVariant 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) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(16.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)) Spacer(modifier = Modifier.height(16.dp))
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
@@ -52,9 +52,10 @@ class ReadRecordOverviewViewModel(
_period, _period,
_referenceDate, _referenceDate,
repository.getAllRecordDetails(""), repository.getAllRecordDetails(""),
repository.getLatestReadRecords("") repository.getLatestReadRecords(""),
) { period, refDate, details, latestRecords -> bookRepository.getAllBooks()
getReadRecordOverviewUseCase(period, refDate, details, latestRecords) ) { period, refDate, details, latestRecords, allBooks ->
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), started = SharingStarted.WhileSubscribed(5000),
@@ -2,8 +2,6 @@ package io.legado.app.ui.book.readRecord
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility 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.basicMarquee
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -49,14 +47,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset 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.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString 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.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.data.entities.readRecord.ReadRecordSession 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.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
import io.legado.app.ui.theme.adaptiveHorizontalPadding 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.CollapsibleHeader
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.alert.AppAlertDialog 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.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.GlassCard
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.checkBox.CheckboxItem 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.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults 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.StringUtils.formatFriendlyDate
import io.legado.app.utils.formatReadDuration
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.time.LocalDate 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( Column(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -556,31 +526,7 @@ fun HeatmapCalendarSection(
horizontalArrangement = Arrangement.spacedBy(config.cellSpacing), horizontalArrangement = Arrangement.spacedBy(config.cellSpacing),
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .fadingEdge(listState, config.gradientWidth)
.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
)
}
) { ) {
val firstReadDate = listOfNotNull( val firstReadDate = listOfNotNull(
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(), dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
@@ -1,15 +1,35 @@
package io.legado.app.ui.book.readRecord.component 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.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.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.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.legado.app.ui.book.readRecord.ReadPeriod 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.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
@@ -22,7 +42,16 @@ fun ReadingTimeBarChartCard(
period: ReadPeriod, period: ReadPeriod,
modifier: Modifier = Modifier 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( GlassCard(
modifier = modifier modifier = modifier
@@ -30,7 +59,16 @@ fun ReadingTimeBarChartCard(
.adaptiveHorizontalPadding(vertical = 8.dp) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(16.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)) Spacer(modifier = Modifier.height(16.dp))
Row( Row(
@@ -41,13 +79,14 @@ fun ReadingTimeBarChartCard(
// Y-Axis // Y-Axis
Column( Column(
modifier = Modifier modifier = Modifier
.width(32.dp)
.fillMaxHeight() .fillMaxHeight()
.padding(bottom = 20.dp), .padding(bottom = 20.dp),
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.End horizontalAlignment = Alignment.End
) { ) {
AppText( AppText(
text = formatChartDuration(maxTime), text = ReadRecordFormatter.formatDuration(roundedMaxTime),
style = LegadoTheme.typography.labelSmall, style = LegadoTheme.typography.labelSmall,
fontSize = 8.sp, fontSize = 8.sp,
color = LegadoTheme.colorScheme.onSurfaceVariant color = LegadoTheme.colorScheme.onSurfaceVariant
@@ -71,7 +110,12 @@ fun ReadingTimeBarChartCard(
horizontalArrangement = Arrangement.SpaceEvenly horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
data.forEachIndexed { index, (date, time) -> 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) { val showLabel = when (period) {
ReadPeriod.DAY -> true ReadPeriod.DAY -> true
@@ -98,6 +142,7 @@ fun ReadingTimeBarChartCard(
Box( Box(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.widthIn(max = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
@@ -109,7 +154,7 @@ fun ReadingTimeBarChartCard(
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp)) .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
.background( .background(
if (time > 0) LegadoTheme.colorScheme.primary 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, text = labelText,
style = LegadoTheme.typography.labelSmall, style = LegadoTheme.typography.labelSmall,
fontSize = 8.sp, 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 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.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.card.GlassCard 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 import io.legado.app.ui.widget.components.text.AppText
data class StatItem(val label: String, val value: String) data class StatItem(val label: String, val value: String)
@@ -26,12 +43,24 @@ fun StatsGridCard(
.adaptiveHorizontalPadding(vertical = 8.dp) .adaptiveHorizontalPadding(vertical = 8.dp)
) { ) {
Column(modifier = Modifier.padding(16.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)) Spacer(modifier = Modifier.height(16.dp))
Column { Column {
for (i in items.indices step 2) { 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)) StatCell(items[i], Modifier.weight(1f))
if (i + 1 < items.size) { if (i + 1 < items.size) {
StatCell(items[i + 1], Modifier.weight(1f)) StatCell(items[i + 1], Modifier.weight(1f))
@@ -55,18 +84,26 @@ private fun StatCell(
modifier = modifier, modifier = modifier,
horizontalAlignment = Alignment.Start horizontalAlignment = Alignment.Start
) { ) {
AppText(
text = item.value,
style = LegadoTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = LegadoTheme.colorScheme.primary,
textAlign = TextAlign.Start
)
AppText( AppText(
text = item.label, text = item.label,
style = LegadoTheme.typography.labelSmall, style = LegadoTheme.typography.labelSmall,
color = LegadoTheme.colorScheme.onSurfaceVariant, color = LegadoTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Start 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 -> { MainRouteReadRecord -> {
if (currentRoute == MainRouteHome) { if (currentRoute == MainRouteHome) {
backStack.add(route) backStack.add(route)
@@ -5,13 +5,16 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape 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.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.SourceIcon import io.legado.app.ui.widget.components.SourceIcon
import io.legado.app.ui.widget.components.alert.AppAlertDialog 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.PillDivider
import io.legado.app.ui.widget.components.divider.PillHeaderDivider 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.list.ListScaffold
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon 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.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem 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.text.AppText
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@@ -136,18 +140,6 @@ fun RssScreen(
onSearchToggle = { viewModel.toggleSearchVisible(it) }, onSearchToggle = { viewModel.toggleSearchVisible(it) },
onSearchQueryChange = { viewModel.search(it) }, onSearchQueryChange = { viewModel.search(it) },
searchPlaceholder = stringResource(R.string.search_rss_source), 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 -> dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem( RoundDropdownMenuItem(
onClick = { onClick = {
@@ -185,6 +177,58 @@ fun RssScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = 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 -> items(uiState.items, key = { it.sourceUrl }) { source ->
RssSourceGridItem( RssSourceGridItem(
modifier = Modifier.animateItem(), modifier = Modifier.animateItem(),
@@ -4,8 +4,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -86,11 +84,7 @@ fun RuleSubScreen(
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.sort), text = stringResource(R.string.sort),
leadingIcon = { leadingIcon = {
Row(verticalAlignment = Alignment.CenterVertically) { Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.size(12.dp))
AppText(stringResource(R.string.sort))
}
}, },
onClick = { onClick = {
viewModel.resetOrder() viewModel.resetOrder()
@@ -231,12 +225,14 @@ fun RuleSubEditDialog(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
label = stringResource(R.string.name), label = stringResource(R.string.name),
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
AppTextField( AppTextField(
value = url, value = url,
onValueChange = { url = it }, onValueChange = { url = it },
label = "URL", label = "URL",
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
modifier = Modifier.fillMaxWidth() 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( Icon(
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription contentDescription = contentDescription,
tint = LegadoTheme.colorScheme.onSurface
) )
} }
} }
@@ -1,10 +1,9 @@
package io.legado.app.ui.widget.components.tabRow package io.legado.app.ui.widget.components.tabRow
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -12,6 +11,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver 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.TabRowDefaults
import top.yukonga.miuix.kmp.basic.TabRowWithContour import top.yukonga.miuix.kmp.basic.TabRowWithContour
@@ -20,7 +20,8 @@ fun AppTabRow(
tabTitles: List<String>, tabTitles: List<String>,
selectedTabIndex: Int, selectedTabIndex: Int,
onTabSelected: (Int) -> Unit, onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
isScrollable: Boolean = true
) { ) {
val composeEngine = LegadoTheme.composeEngine val composeEngine = LegadoTheme.composeEngine
@@ -36,29 +37,59 @@ fun AppTabRow(
) )
) )
} else { } else {
PrimaryScrollableTabRow( if (isScrollable) {
selectedTabIndex = selectedTabIndex, PrimaryScrollableTabRow(
edgePadding = 0.dp, selectedTabIndex = selectedTabIndex,
divider = { }, edgePadding = 0.dp,
containerColor = Color.Transparent, divider = { },
minTabWidth = 0.dp, containerColor = Color.Transparent,
modifier = modifier minTabWidth = 0.dp,
) { modifier = modifier
tabTitles.forEachIndexed { index, title -> ) {
Tab( tabTitles.forEachIndexed { index, title ->
selected = selectedTabIndex == index, AppTab(
onClick = { onTabSelected(index) }, selected = selectedTabIndex == index,
text = { onClick = { onTabSelected(index) },
Text( title = title
text = title, )
maxLines = 1, }
overflow = TextOverflow.Ellipsis, }
modifier = Modifier.padding(horizontal = 8.dp), } else {
color = MaterialTheme.colorScheme.onSurfaceVariant 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。 //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt() val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt()
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 //计算原理是,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 @JvmOverloads