界面优化
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user