界面优化

This commit is contained in:
HapeLee
2026-05-17 04:59:39 +08:00
parent 1ac5f6549b
commit 5987e3414c
11 changed files with 283 additions and 157 deletions
@@ -33,13 +33,11 @@ import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -48,10 +46,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -73,7 +69,6 @@ import io.legado.app.ui.book.search.ScopeSelectSheet
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -86,7 +81,8 @@ 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.tabRow.AppTabRow
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.startActivity
@@ -103,7 +99,11 @@ fun WebFileSheet(
) {
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest, title = title) {
if (files.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp), contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.empty))
}
} else {
@@ -111,7 +111,9 @@ fun WebFileSheet(
items(files, key = { it.name }) { file ->
GlassCard(onClick = { onSelect(file) }) {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
@@ -269,6 +271,31 @@ fun ChangeCoverSheet(
}
}
@Composable
fun ChangeSourceSheet(
data: Book?,
onDismissRequest: () -> Unit,
onReplace: (BookSource, Book, List<BookChapter>, ChangeSourceMigrationOptions) -> Unit,
onAddAsNew: (Book, List<BookChapter>) -> Unit,
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) {
cachedData = data
}
val currentData = cachedData
if (currentData != null) {
ChangeSourceSheet(
show = data != null,
oldBook = currentData,
onDismissRequest = onDismissRequest,
onReplace = onReplace,
onAddAsNew = onAddAsNew,
)
}
}
@Composable
fun ChangeSourceSheet(
show: Boolean,
@@ -426,7 +453,9 @@ fun ChangeSourceSheet(
if (items.isEmpty()) {
Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
contentAlignment = Alignment.Center
) {
EmptyMessage(
@@ -10,7 +10,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.fillMaxSize
@@ -27,6 +26,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Info
@@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Checkbox
@@ -54,16 +53,19 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
import io.legado.app.help.book.getExportFileName
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.tryParesExportFileName
@@ -80,11 +82,9 @@ import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.FabMenuItem
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.card.GlassCard
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
import io.legado.app.ui.widget.components.card.TextCard
@@ -805,28 +805,25 @@ private fun BookshelfManageScreen(
}
}
singleChangeSourceBook?.let { book ->
ChangeSourceSheet(
show = true,
oldBook = book,
onDismissRequest = { singleChangeSourceBook = null },
onReplace = { source, newBook, toc, options ->
viewModel.dispatch(
BookshelfManageScreenIntent.ChangeBookSource(
oldBookUrl = book.bookUrl,
source = source,
book = newBook,
chapters = toc,
options = options,
)
ChangeSourceSheet(
data = singleChangeSourceBook,
onDismissRequest = { singleChangeSourceBook = null },
onReplace = { source: BookSource, newBook: Book, toc: List<BookChapter>, options: ChangeSourceMigrationOptions ->
viewModel.dispatch(
BookshelfManageScreenIntent.ChangeBookSource(
oldBookUrl = singleChangeSourceBook?.bookUrl ?: "",
source = source,
book = newBook,
chapters = toc,
options = options,
)
singleChangeSourceBook = null
},
onAddAsNew = { _, _ ->
context.toastOnUi("请在书籍详情页添加为新书")
},
)
}
)
singleChangeSourceBook = null
},
onAddAsNew = { _: Book, _: List<BookChapter> ->
context.toastOnUi("请在书籍详情页添加为新书")
},
)
BookSourcePickerSheet(
show = showBatchSourcePickerSheet,
@@ -857,8 +854,7 @@ private fun BookshelfManageScreen(
)
BatchChangePreviewSheet(
show = state.batchChangePreviewItems.isNotEmpty(),
items = state.batchChangePreviewItems,
data = state.batchChangePreviewItems.takeIf { it.isNotEmpty() },
onDismissRequest = { viewModel.dispatch(BookshelfManageScreenIntent.DismissBatchChangePreview) },
onOpenBook = { book, inBookshelf ->
viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, inBookshelf))
@@ -871,29 +867,27 @@ private fun BookshelfManageScreen(
},
onShowOtherSources = { item -> otherSourcePreviewItem = item },
onMigrateAll = { viewModel.dispatch(BookshelfManageScreenIntent.MigrateAllPreviewItems) },
onAddAllToShelf = { viewModel.dispatch(BookshelfManageScreenIntent.AddAllPreviewItemsToShelf) },
)
manualSearchPreviewBook?.let { book ->
ChangeSourceSheet(
show = true,
oldBook = book,
onDismissRequest = { manualSearchPreviewBook = null },
onReplace = { source, newBook, toc, _ ->
viewModel.dispatch(
BookshelfManageScreenIntent.UpdatePreviewItem(
oldBookUrl = book.bookUrl,
source = source,
book = newBook,
chapterCount = toc.size,
)
ChangeSourceSheet(
data = manualSearchPreviewBook,
onDismissRequest = { manualSearchPreviewBook = null },
onReplace = { source: BookSource, newBook: Book, toc: List<BookChapter>, _ ->
viewModel.dispatch(
BookshelfManageScreenIntent.UpdatePreviewItem(
oldBookUrl = manualSearchPreviewBook?.bookUrl ?: "",
source = source,
book = newBook,
chapterCount = toc.size,
)
manualSearchPreviewBook = null
},
onAddAsNew = { _, _ ->
context.toastOnUi("请先选择替换候选后再新增至书架")
},
)
}
)
manualSearchPreviewBook = null
},
onAddAsNew = { _: Book, _: List<BookChapter> ->
context.toastOnUi("请先选择替换候选后再新增至书架")
},
)
OtherSourceOptionsSheet(
item = otherSourcePreviewItem,
@@ -1392,8 +1386,7 @@ private fun SourcePickerItem(
@Composable
private fun BatchChangePreviewSheet(
show: Boolean,
items: List<BatchChangeSourcePreviewItem>,
data: List<BatchChangeSourcePreviewItem>?,
onDismissRequest: () -> Unit,
onOpenBook: (Book, Boolean) -> Unit,
onManualSearch: (Book) -> Unit,
@@ -1402,11 +1395,19 @@ private fun BatchChangePreviewSheet(
onAddToShelf: (String) -> Unit,
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
onMigrateAll: () -> Unit,
onAddAllToShelf: () -> Unit,
) {
AppModalBottomSheet(
show = show,
data = data,
onDismissRequest = onDismissRequest,
title = "批量换源预览",
startAction = {
SmallTonalTextButton(
text = "新增全部",
imageVector = Icons.Default.Add,
onClick = onAddAllToShelf,
)
},
endAction = {
SmallTonalTextButton(
text = "迁移全部",
@@ -1414,7 +1415,7 @@ private fun BatchChangePreviewSheet(
onClick = onMigrateAll,
)
}
) {
) { items ->
LazyColumn(
modifier = Modifier
.fillMaxWidth()
@@ -1449,15 +1450,15 @@ private fun BatchChangePreviewRow(
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
) {
val candidate = item.selectedCandidate
NormalCard(
GlassCard(
modifier = Modifier.fillMaxWidth(),
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
containerColor = LegadoTheme.colorScheme.onSheetContent,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -1493,27 +1494,26 @@ private fun BatchChangePreviewRow(
modifier = Modifier.weight(1f),
)
}
FlowRow(
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
MediumIconButton(
SmallTonalTextButton(
imageVector = Icons.Default.Search,
onClick = { onManualSearch(item.oldBook) },
)
SmallTonalTextButton(
text = "不迁移",
imageVector = Icons.Default.SkipNext,
imageVector = Icons.Default.Clear,
onClick = { onSkip(item.oldBook.bookUrl) },
)
SmallTonalTextButton(
text = "开始迁移",
text = "迁移",
imageVector = Icons.Default.PlayArrow,
onClick = { onMigrate(item.oldBook.bookUrl) },
)
SmallTonalTextButton(
text = "新增至书架",
text = "新增",
imageVector = Icons.Default.Add,
onClick = { onAddToShelf(item.oldBook.bookUrl) },
)
@@ -1553,7 +1553,8 @@ private fun PreviewBookInfo(
AppText(
text = book.name,
style = LegadoTheme.typography.labelMedium,
maxLines = 1,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
AppText(
text = "${book.getRealAuthor()} · ${chapterCount ?: 0}",
@@ -1566,8 +1567,7 @@ private fun PreviewBookInfo(
AppText(
text = title,
style = LegadoTheme.typography.labelMedium,
color = LegadoTheme.colorScheme.error,
maxLines = 2,
color = LegadoTheme.colorScheme.error
)
}
}
@@ -1581,41 +1581,38 @@ private fun OtherSourceOptionsSheet(
onOpenBook: (Book) -> Unit,
) {
AppModalBottomSheet(
show = item != null,
data = item,
onDismissRequest = onDismissRequest,
title = "其他源信息",
) {
val currentItem = item
if (currentItem != null) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 560.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(currentItem.candidates.indices.toList(), key = { it }) { index ->
val candidate = currentItem.candidates[index]
SelectionItemCard(
title = candidate.source.bookSourceName,
subtitle = "${candidate.book.name} · ${candidate.chapterCount}",
supportingContent = {
AppText(
text = candidate.book.getRealAuthor(),
style = LegadoTheme.typography.bodySmall,
)
},
isSelected = index == currentItem.selectedCandidateIndex,
onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
trailingAction = {
SmallTonalIconButton(
onClick = { onOpenBook(candidate.book) },
imageVector = Icons.Default.Info,
contentDescription = null,
)
},
containerColor = LegadoTheme.colorScheme.onSheetContent,
)
}
) { currentItem ->
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 560.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(currentItem.candidates.indices.toList(), key = { it }) { index ->
val candidate = currentItem.candidates[index]
SelectionItemCard(
title = candidate.source.bookSourceName,
subtitle = "${candidate.book.name} · ${candidate.chapterCount}",
supportingContent = {
AppText(
text = candidate.book.getRealAuthor(),
style = LegadoTheme.typography.bodySmall,
)
},
isSelected = index == currentItem.selectedCandidateIndex,
onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
trailingAction = {
SmallTonalIconButton(
onClick = { onOpenBook(candidate.book) },
imageVector = Icons.Default.Info,
contentDescription = null,
)
},
containerColor = LegadoTheme.colorScheme.onSheetContent,
)
}
}
Spacer(modifier = Modifier.height(16.dp))
@@ -3,19 +3,19 @@ package io.legado.app.ui.book.manage
import android.app.Application
import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao
import io.legado.app.data.dao.BookGroupDao
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.BookSource
import io.legado.app.constant.BookType
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.BatchChangeSourceCandidate
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
@@ -23,11 +23,10 @@ import io.legado.app.domain.usecase.ClearBookCacheUseCase
import io.legado.app.domain.usecase.DeleteBooksUseCase
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType
import io.legado.app.model.CacheBook
import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CacheBook
import io.legado.app.service.ExportBookService
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
@@ -39,17 +38,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
import kotlin.math.min
import java.util.concurrent.ConcurrentHashMap
data class BookshelfManageScreenExportConfig(
val exportUseReplace: Boolean = true,
@@ -123,6 +122,7 @@ sealed interface BookshelfManageScreenIntent {
data class AddPreviewItemToShelf(val oldBookUrl: String) : BookshelfManageScreenIntent
data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : BookshelfManageScreenIntent
data object MigrateAllPreviewItems : BookshelfManageScreenIntent
data object AddAllPreviewItemsToShelf : BookshelfManageScreenIntent
data object DismissChangeSourceStatus : BookshelfManageScreenIntent
data object DismissBatchChangePreview : BookshelfManageScreenIntent
data class SetExportUseReplace(val enabled: Boolean) : BookshelfManageScreenIntent
@@ -233,6 +233,8 @@ class BookshelfManageScreenViewModel(
BookshelfManageScreenIntent.MigrateAllPreviewItems -> migrateAllPreviewItems()
BookshelfManageScreenIntent.AddAllPreviewItemsToShelf -> addAllPreviewItemsToShelf()
BookshelfManageScreenIntent.DismissChangeSourceStatus -> {
_uiState.update {
it.copy(
@@ -958,6 +960,45 @@ class BookshelfManageScreenViewModel(
}
}
private fun addAllPreviewItemsToShelf() {
val items = uiState.value.batchChangePreviewItems.filter { it.canMigrate }
if (items.isEmpty()) return
execute {
_uiState.update {
it.copy(isChangingSource = true, changeSourceProgress = "0 / ${items.size}")
}
items.forEachIndexed { index, item ->
_uiState.update {
it.copy(changeSourceProgress = "${index + 1} / ${items.size} ${item.oldBook.name}")
}
val candidate = item.selectedCandidate ?: return@forEachIndexed
val chapters = changeBookSourceUseCase.loadCandidateChapters(
candidate.source,
candidate.book
) ?: return@forEachIndexed
candidate.book.removeType(BookType.notShelf)
if (candidate.book.order == 0) {
candidate.book.order = bookDao.minOrder - 1
}
bookDao.insert(candidate.book)
bookChapterDao.insert(*chapters.toTypedArray())
}
}.onSuccess {
_uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量添加完成"))
}.onError {
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量添加失败\n${it.localizedMessage}"))
}.onFinally {
_uiState.update {
it.copy(
isChangingSource = false,
changeSourceProgress = null,
cacheVersion = it.cacheVersion + 1
)
}
}
}
private fun clearCacheForBook(book: Book) {
execute {
clearBookCacheUseCase.execute(book.bookUrl)
@@ -151,7 +151,7 @@ fun BookshelfItem(
)
)
)
.padding(horizontal = 6.dp, vertical = 6.dp)
.padding(all = 4.dp)
)
}
}
@@ -165,7 +165,7 @@ fun BookshelfItem(
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp, start = 4.dp, end = 4.dp, bottom = 8.dp)
.padding(start = 4.dp, end = 4.dp, bottom = 4.dp)
)
}
}
@@ -175,7 +175,7 @@ fun BookshelfItem(
NormalCard(
modifier = modifier
.fillMaxWidth()
.padding(all = 4.dp),
.padding(vertical = 4.dp),
cornerRadius = 8.dp,
containerColor = if (isSelected) {
LegadoTheme.colorScheme.secondaryContainer
@@ -194,7 +194,6 @@ fun BookshelfItem(
modifier = Modifier
.align(Alignment.Top)
.width(if (!isCompact) 84.dp else 56.dp)
.padding(end = 8.dp)
) {
Box(
modifier = Modifier
@@ -215,7 +214,7 @@ fun BookshelfItem(
Column(
modifier = Modifier
.weight(1f)
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
.padding(top = 4.dp, bottom = 4.dp, end = 4.dp),
verticalArrangement = Arrangement.Center
) {
Row(
@@ -524,13 +523,13 @@ fun BookGroupItemHorizontalCovers(
) {
Column(
modifier = Modifier
.padding(vertical = 12.dp),
.padding(vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, top = 4.dp, bottom = 4.dp),
.padding(start = 4.dp, top = 4.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -549,7 +548,7 @@ fun BookGroupItemHorizontalCovers(
)
}
AppIcon(
modifier = Modifier.padding(end = 6.dp),
modifier = Modifier.padding(end = 4.dp),
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "",
tint = LegadoTheme.colorScheme.onSurfaceVariant
@@ -558,8 +557,8 @@ fun BookGroupItemHorizontalCovers(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
.padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
val coverCount = BookshelfConfig.bookshelfGroupCoverCount
previewBooks.take(coverCount).forEach { bookUi ->
@@ -781,7 +781,7 @@ fun BookshelfScreen(
contentPadding = adaptiveContentPaddingBookshelf(
top = paddingValues.calculateTopPadding(),
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
horizontal = if (isGridMode) 8.dp else 4.dp
horizontal = 4.dp
),
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -1361,7 +1361,7 @@ fun BookshelfPage(
contentPadding = adaptiveContentPaddingBookshelf(
top = paddingValues.calculateTopPadding(),
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
horizontal = if (isGridMode) 8.dp else 4.dp
horizontal = 8.dp
),
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -175,6 +175,13 @@ class BookshelfViewModel(
val allBookCount: Int
)
private data class DataForPreviews(
val groups: List<BookGroup>,
val bookGroupStyle: Int,
val systemCountsMap: Map<Long, Int>,
val allBookCount: Int
)
val groupSelectorState: StateFlow<BookshelfGroupSelectorState> = combine(
groupsFlow,
groupIdFlow
@@ -262,16 +269,25 @@ class BookshelfViewModel(
private val groupPreviewsFlow = combine(
groupsFlow,
bookGroupStyleFlow,
appDb.bookDao.flowSystemGroupCounts()
) { groups, bookGroupStyle, systemCounts ->
Triple(groups, bookGroupStyle, systemCounts.associate { it.groupId to it.count })
}.flatMapLatest { (groups, bookGroupStyle, systemCountsMap) ->
appDb.bookDao.flowSystemGroupCounts(),
appDb.bookDao.flowAllBookShelfCount()
) { groups, bookGroupStyle, systemCounts, totalCount ->
DataForPreviews(
groups,
bookGroupStyle,
systemCounts.associate { it.groupId to it.count },
totalCount
)
}.flatMapLatest { data ->
val groups = data.groups
val bookGroupStyle = data.bookGroupStyle
val systemCountsMap = data.systemCountsMap
val allBookCount = data.allBookCount
if (bookGroupStyle !in 2..3) {
appDb.bookDao.flowAllBookShelfCount().map { count ->
GroupPreviewState(persistentMapOf(), persistentMapOf(), count)
}
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
} else if (groups.isEmpty()) {
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), 0))
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
} else {
val groupFlows = groups.map { group ->
val countFlow: Flow<Int> = if (group.groupId > 0) {
@@ -287,11 +303,9 @@ class BookshelfViewModel(
combine(groupFlows) { results ->
var previews = persistentMapOf<Long, ImmutableList<BookUiItem>>()
var counts = persistentMapOf<Long, Int>()
var allBookCount = 0
results.forEach { (groupId, count, preview) ->
counts = counts.put(groupId, count)
previews = previews.put(groupId, preview.toImmutableList())
if (groupId == BookGroup.IdAll) allBookCount = count
}
GroupPreviewState(previews, counts, allBookCount)
}
@@ -480,10 +494,16 @@ class BookshelfViewModel(
bookshelfSort = internal.sortConfig.sort,
bookshelfSortOrder = internal.sortConfig.sortOrder,
title = title,
subtitle = if (interaction.isEditMode) {
context.getString(R.string.bookshelf_total_count, previews.allBookCount)
} else {
null
subtitle = when {
interaction.isEditMode -> {
context.getString(R.string.bookshelf_total_count, previews.allBookCount)
}
internal.isSearchMode -> {
context.getString(R.string.bookshelf_total_count, filteredBooks.size)
}
else -> null
},
currentGroupName = currentGroupName,
draggingBooks = interaction.draggingBooks?.toImmutableList(),
@@ -86,7 +86,8 @@ fun adaptiveContentPaddingBookshelf(
horizontal: Dp
): PaddingValues {
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 8.dp
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp + horizontal else 4.dp + horizontal
val horizontal =
if (ThemeResolver.isMiuixEngine(composeEngine)) 6.dp + horizontal else 4.dp + horizontal
return PaddingValues(
top = adjustedTop,
bottom = bottom,
@@ -32,10 +32,10 @@ import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
import top.yukonga.miuix.kmp.theme.MiuixTheme
private val SmallMiuixButtonSize = 32.dp
private val SmallMiuixIconSize = 18.dp
@@ -74,7 +74,7 @@ fun SmallIconButton(
MiuixIcon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
modifier = Modifier.size(16.dp),
)
}
} else {
@@ -4,8 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
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
@@ -24,8 +22,6 @@ import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Button as MiuixButton
import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
@Composable
@@ -85,7 +81,7 @@ fun SmallTextButton(
@Composable
fun SmallTonalTextButton(
text: String,
text: String? = null,
imageVector: ImageVector,
modifier: Modifier = Modifier,
onClick: () -> Unit
@@ -111,10 +107,12 @@ fun SmallTonalTextButton(
contentDescription = null,
modifier = Modifier.size(16.dp)
)
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
if (text != null) {
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
} else {
@@ -129,10 +127,12 @@ fun SmallTonalTextButton(
modifier = Modifier.size(16.dp)
)
Spacer(Modifier.width(4.dp))
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
if (text != null) {
AppText(
text = text,
style = LegadoTheme.typography.labelMedium
)
}
}
}
}
@@ -19,9 +19,12 @@ import androidx.compose.material3.Typography
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextAlign
@@ -165,3 +168,39 @@ fun AppModalBottomSheet(
}
}
}
/**
* 专为 nullable 数据设计的 AppModalBottomSheet 重载。
* 当 [data] 不为 null 时显示弹窗;当 [data] 变为 null 时,自动缓存最后一次数据并播放退出动画。
*/
@Composable
fun <T> AppModalBottomSheet(
data: T?,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
startAction: @Composable (() -> Unit)? = null,
endAction: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.(T) -> Unit
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) {
cachedData = data
}
val currentData = cachedData
AppModalBottomSheet(
show = data != null,
onDismissRequest = onDismissRequest,
modifier = modifier,
title = title,
startAction = startAction,
endAction = endAction,
content = {
if (currentData != null) {
content(currentData)
}
}
)
}