[优化] 优化界面
This commit is contained in:
@@ -13,6 +13,8 @@ import io.legado.app.data.repository.ExploreRepository
|
||||
import io.legado.app.data.repository.ExploreRepositoryImpl
|
||||
import io.legado.app.data.repository.ReadRecordRepository
|
||||
import io.legado.app.data.repository.RemoteBookRepository
|
||||
import io.legado.app.data.repository.SearchRepository
|
||||
import io.legado.app.data.repository.SearchRepositoryImpl
|
||||
import io.legado.app.data.repository.SearchContentRepository
|
||||
import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
@@ -30,6 +32,7 @@ import io.legado.app.ui.book.import.local.ImportBookViewModel
|
||||
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
|
||||
import io.legado.app.ui.book.info.BookInfoViewModel
|
||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||
import io.legado.app.ui.book.search.SearchViewModel
|
||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||
import io.legado.app.ui.book.toc.TocViewModel
|
||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
||||
@@ -69,6 +72,7 @@ val appModule = module {
|
||||
|
||||
single<UploadRepository> { DirectLinkUploadRepository() }
|
||||
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
||||
single<SearchRepository> { SearchRepositoryImpl(get()) }
|
||||
|
||||
single<ImageLoader> {
|
||||
ImageLoader.Builder(get())
|
||||
@@ -111,6 +115,7 @@ val appModule = module {
|
||||
viewModelOf(::ChangeBookSourceViewModel)
|
||||
viewModelOf(::ExploreViewModel)
|
||||
viewModelOf(::RssViewModel)
|
||||
viewModelOf(::SearchViewModel)
|
||||
|
||||
viewModel { (route: ReplaceEditRoute) ->
|
||||
ReplaceEditViewModel(
|
||||
|
||||
@@ -50,10 +50,9 @@ import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SearchBarSection
|
||||
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
|
||||
import io.legado.app.ui.widget.components.bookmark.BookmarkItem
|
||||
@@ -212,7 +211,7 @@ fun AllBookmarkScreen(
|
||||
) { state ->
|
||||
when (state) {
|
||||
"LOADING" -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "加载中...",
|
||||
isLoading = true,
|
||||
modifier = Modifier
|
||||
@@ -225,7 +224,7 @@ fun AllBookmarkScreen(
|
||||
}
|
||||
|
||||
"EMPTY" -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "没有书签!",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -92,6 +92,8 @@ import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
|
||||
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.book.SearchBookGridItem
|
||||
import io.legado.app.ui.widget.components.book.SearchBookListItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
@@ -521,111 +523,12 @@ fun ExploreBookItem(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
|
||||
|
||||
val badge: (@Composable RowScope.() -> Unit)?
|
||||
= when (shelfState) {
|
||||
|
||||
BookShelfState.IN_SHELF -> {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "已在书架",
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
BookShelfState.SAME_NAME_AUTHOR -> {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Default.Warning,
|
||||
contentDescription = "同名书籍",
|
||||
modifier = Modifier.size(12.dp)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BookShelfState.NOT_IN_SHELF -> null
|
||||
}
|
||||
|
||||
Row(
|
||||
SearchBookListItem(
|
||||
book = book,
|
||||
shelfState = shelfState,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
|
||||
Cover(
|
||||
path = book.coverUrl,
|
||||
modifier = Modifier.width(72.dp),
|
||||
badgeContent = badge)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Column(modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)) {
|
||||
|
||||
AppText(
|
||||
text = book.name,
|
||||
style = LegadoTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Row {
|
||||
AppText(
|
||||
text = book.author,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
val latestChapter = book.latestChapterTitle
|
||||
if (!latestChapter.isNullOrEmpty()) {
|
||||
AppText(
|
||||
text = " • ",
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
AppText(
|
||||
text = "最新: $latestChapter",
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val intro = book.intro?.replace("\\s+".toRegex(), "") ?: ""
|
||||
if (intro.isNotEmpty()) {
|
||||
AppText(
|
||||
text = intro,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 2,
|
||||
minLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
val kinds = book.getKindList()
|
||||
if (kinds.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
|
||||
kinds.forEach { kind ->
|
||||
TagChip(text = kind)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -635,71 +538,12 @@ fun ExploreBookGridItem(
|
||||
shelfState: Flow<BookShelfState>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
||||
val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
|
||||
|
||||
val badgeText: String? = when (shelfState) {
|
||||
BookShelfState.IN_SHELF -> "已在书架"
|
||||
BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
|
||||
BookShelfState.NOT_IN_SHELF -> null
|
||||
}
|
||||
|
||||
val content: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) {
|
||||
{
|
||||
AppText(
|
||||
text = badgeText,
|
||||
style = LegadoTheme.typography.labelSmall.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 9.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
Column(
|
||||
SearchBookGridItem(
|
||||
book = book,
|
||||
shelfState = shelfState,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.width(IntrinsicSize.Min)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable(onClick = onClick)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
|
||||
Cover(
|
||||
path = book.coverUrl,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(12 / 17f),
|
||||
badgeContent = content
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
AppText(
|
||||
text = book.name,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的标签组件
|
||||
@Composable
|
||||
fun TagChip(text: String) {
|
||||
Surface(
|
||||
color = LegadoTheme.colorScheme.cardContainer,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = text,
|
||||
modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
color = LegadoTheme.colorScheme.onCardContainer
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
|
||||
@@ -8,18 +8,16 @@ 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.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Restore
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedIconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@@ -34,12 +32,16 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
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.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.cover.BookCover
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.MD5Utils
|
||||
import io.legado.app.utils.SelectImageContract
|
||||
@@ -58,10 +60,35 @@ fun GroupEditSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
viewModel: GroupViewModel = koinViewModel()
|
||||
) {
|
||||
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) {
|
||||
var coverPath by remember(group) { mutableStateOf(group?.cover) }
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
startAction = if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
|
||||
{
|
||||
GroupDeleteAction(
|
||||
group = group,
|
||||
onDismissRequest = onDismissRequest,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endAction = {
|
||||
GroupResetCoverAction(
|
||||
group = group,
|
||||
onCoverPathChange = { coverPath = it },
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
) {
|
||||
GroupEditContent(
|
||||
group = group,
|
||||
onDismissRequest = onDismissRequest,
|
||||
coverPath = coverPath,
|
||||
onCoverPathChange = { coverPath = it },
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
@@ -71,11 +98,12 @@ fun GroupEditSheet(
|
||||
fun GroupEditContent(
|
||||
group: BookGroup? = null,
|
||||
onDismissRequest: () -> Unit,
|
||||
coverPath: String?,
|
||||
onCoverPathChange: (String?) -> Unit,
|
||||
viewModel: GroupViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") }
|
||||
var coverPath by remember(group) { mutableStateOf(group?.cover) }
|
||||
var enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) }
|
||||
var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
|
||||
|
||||
@@ -96,7 +124,7 @@ fun GroupEditContent(
|
||||
FileOutputStream(file).use { output ->
|
||||
context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) }
|
||||
}
|
||||
coverPath = file.absolutePath
|
||||
onCoverPathChange(file.absolutePath)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
appCtx.toastOnUi(e.localizedMessage)
|
||||
@@ -107,6 +135,7 @@ fun GroupEditContent(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
@@ -129,23 +158,29 @@ fun GroupEditContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
AppTextField(
|
||||
value = groupName,
|
||||
onValueChange = { groupName = it },
|
||||
label = { AppText(stringResource(R.string.group_name)) },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = stringResource(R.string.group_name),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.sort),
|
||||
selectedValue = selectedSortIndex.toString(),
|
||||
displayEntries = sortOptions,
|
||||
entryValues = sortEntryValues,
|
||||
onValueChange = {
|
||||
selectedSortIndex = it.toInt()
|
||||
}
|
||||
)
|
||||
GlassCard(
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
) {
|
||||
CompactDropdownSettingItem(
|
||||
title = stringResource(R.string.sort),
|
||||
selectedValue = selectedSortIndex.toString(),
|
||||
color = LegadoTheme.colorScheme.onSheetContent,
|
||||
displayEntries = sortOptions,
|
||||
entryValues = sortEntryValues,
|
||||
onValueChange = {
|
||||
selectedSortIndex = it.toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,79 +194,91 @@ fun GroupEditContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
|
||||
OutlinedIconButton(onClick = {
|
||||
context.alert(R.string.delete, R.string.sure_del) {
|
||||
yesButton {
|
||||
viewModel.delGroup(group) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedIconButton(onClick = {
|
||||
if (group != null) {
|
||||
viewModel.clearCover(group) {
|
||||
coverPath = null
|
||||
appCtx.toastOnUi("封面已重置")
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = {
|
||||
if (groupName.isEmpty()) {
|
||||
appCtx.toastOnUi("分组名称不能为空")
|
||||
} else {
|
||||
if (group != null && group.groupId != 0b1L) {
|
||||
viewModel.upGroup(
|
||||
group.copy(
|
||||
groupName = groupName,
|
||||
cover = coverPath,
|
||||
bookSort = selectedSortIndex,
|
||||
enableRefresh = enableRefresh
|
||||
)
|
||||
) {
|
||||
onDismissRequest()
|
||||
}
|
||||
} else {
|
||||
coverPath = null
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = onDismissRequest) {
|
||||
AppText(stringResource(R.string.cancel))
|
||||
}
|
||||
Button(onClick = {
|
||||
if (groupName.isEmpty()) {
|
||||
appCtx.toastOnUi("分组名称不能为空")
|
||||
} else {
|
||||
if (group != null) {
|
||||
viewModel.upGroup(
|
||||
group.copy(
|
||||
groupName = groupName,
|
||||
cover = coverPath,
|
||||
bookSort = selectedSortIndex,
|
||||
enableRefresh = enableRefresh
|
||||
)
|
||||
) {
|
||||
onDismissRequest()
|
||||
}
|
||||
} else {
|
||||
viewModel.addGroup(
|
||||
groupName,
|
||||
selectedSortIndex,
|
||||
enableRefresh,
|
||||
coverPath
|
||||
) {
|
||||
onDismissRequest()
|
||||
}
|
||||
viewModel.addGroup(
|
||||
groupName,
|
||||
selectedSortIndex,
|
||||
enableRefresh,
|
||||
coverPath
|
||||
) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
AppText(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupDeleteAction(
|
||||
group: BookGroup,
|
||||
onDismissRequest: () -> Unit,
|
||||
viewModel: GroupViewModel = koinViewModel()
|
||||
) {
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
|
||||
MediumIconButton(
|
||||
onClick = {
|
||||
showDeleteDialog = true
|
||||
},
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showDeleteDialog,
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
title = stringResource(R.string.delete),
|
||||
text = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
showDeleteDialog = false
|
||||
viewModel.delGroup(group) {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { showDeleteDialog = false }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupResetCoverAction(
|
||||
group: BookGroup? = null,
|
||||
onCoverPathChange: (String?) -> Unit,
|
||||
viewModel: GroupViewModel = koinViewModel()
|
||||
) {
|
||||
MediumIconButton(
|
||||
onClick = {
|
||||
if (group != null) {
|
||||
viewModel.clearCover(group) {
|
||||
onCoverPathChange(null)
|
||||
appCtx.toastOnUi("封面已重置")
|
||||
}
|
||||
} else {
|
||||
onCoverPathChange(null)
|
||||
}
|
||||
},
|
||||
imageVector = Icons.Default.Restore
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package io.legado.app.ui.book.import.local
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.outlined.AddCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
@@ -53,23 +51,15 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.ui.config.importBookConfig.ImportBookConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.permission.Permissions
|
||||
import io.legado.app.lib.permission.PermissionsCompat
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
@@ -83,15 +73,9 @@ import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.isUri
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.takePersistablePermissionSafely
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -222,7 +206,7 @@ private fun ImportBookContent(
|
||||
}
|
||||
|
||||
state.items.isEmpty() -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = stringResource(R.string.empty_msg_import_book)
|
||||
)
|
||||
|
||||
@@ -39,16 +39,11 @@ import androidx.compose.material.icons.outlined.AddCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.CloudSync
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -80,11 +75,14 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.AppRadioButton
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
@@ -93,7 +91,6 @@ 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.text.AppText
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.isUri
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.toastOnUi
|
||||
@@ -173,9 +170,10 @@ fun RemoteBookScreen(
|
||||
},
|
||||
endAction = if (showSheet is RemoteBookSheet.Servers) {
|
||||
{
|
||||
IconButton(onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add))
|
||||
}
|
||||
MediumIconButton(
|
||||
onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
@@ -265,9 +263,10 @@ fun RemoteBookScreen(
|
||||
onSearchQueryChange = { viewModel.dispatch(RemoteBookIntent.SearchChange(it)) },
|
||||
searchPlaceholder = "搜索",
|
||||
topBarActions = {
|
||||
IconButton(onClick = { showSheet = RemoteBookSheet.Servers }) {
|
||||
Icon(Icons.Default.Storage, contentDescription = "配置服务器")
|
||||
}
|
||||
TopBarActionButton(
|
||||
onClick = { showSheet = RemoteBookSheet.Servers },
|
||||
imageVector = Icons.Default.Storage
|
||||
)
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
@@ -335,7 +334,7 @@ fun RemoteBookScreen(
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
@@ -453,7 +452,7 @@ private fun ServerItem(
|
||||
} else {
|
||||
null
|
||||
},
|
||||
containerColor = LegadoTheme.colorScheme.surface,
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
selectedContainerColor = LegadoTheme.colorScheme.secondaryContainer,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -483,6 +482,7 @@ private fun ServerConfigSheetContent(
|
||||
AppTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "名称",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
@@ -491,6 +491,7 @@ private fun ServerConfigSheetContent(
|
||||
AppTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "URL",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
@@ -499,6 +500,7 @@ private fun ServerConfigSheetContent(
|
||||
AppTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "用户名",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
@@ -507,6 +509,7 @@ private fun ServerConfigSheetContent(
|
||||
AppTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = "密码",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
@@ -523,33 +526,23 @@ private fun ServerConfigSheetContent(
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
AppText(stringResource(android.R.string.cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val newServer = server?.copy(name = name) ?: Server(name = name)
|
||||
val newConfig = JSONObject().apply {
|
||||
put("url", url)
|
||||
put("username", username)
|
||||
put("password", password)
|
||||
}
|
||||
newServer.config = newConfig.toString()
|
||||
onSave(newServer)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = name.isNotBlank() && url.isNotBlank()
|
||||
) {
|
||||
AppText("保存")
|
||||
}
|
||||
}
|
||||
onDismiss = onCancel,
|
||||
onConfirm = {
|
||||
val newServer = server?.copy(name = name) ?: Server(name = name)
|
||||
val newConfig = JSONObject().apply {
|
||||
put("url", url)
|
||||
put("username", username)
|
||||
put("password", password)
|
||||
}
|
||||
newServer.config = newConfig.toString()
|
||||
onSave(newServer)
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
confirmText = "保存",
|
||||
confirmEnabled = name.isNotBlank() && url.isNotBlank()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.legado.app.ui.book.info
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -43,6 +42,7 @@ import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Bookmark
|
||||
import androidx.compose.material.icons.outlined.CollectionsBookmark
|
||||
import androidx.compose.material.icons.outlined.FolderZip
|
||||
import androidx.compose.material.icons.outlined.Image
|
||||
@@ -53,13 +53,14 @@ import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MediumFlexibleTopAppBar
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
@@ -78,6 +79,7 @@ import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@@ -89,6 +91,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
@@ -105,11 +108,14 @@ import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.LocalHazeState
|
||||
import io.legado.app.ui.theme.ProvideThemeOverride
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.ThemeOverrideState
|
||||
import io.legado.app.ui.theme.rememberImageSeedColor
|
||||
import io.legado.app.ui.theme.rememberThemeOverride
|
||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
@@ -123,10 +129,12 @@ import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
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.cover.buildCoverImageRequest
|
||||
import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||
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.GlassTopAppBarScrollBehavior
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior
|
||||
import io.legado.app.ui.widget.components.topbar.MiuixGlassScrollBehavior
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
import io.legado.app.utils.startActivity
|
||||
@@ -134,6 +142,7 @@ import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.delay
|
||||
import org.koin.compose.koinInject
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -162,32 +171,12 @@ private fun BookInfoScreenContent(
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
val miuixScrollBehavior = if (isMiuix) {
|
||||
GlassTopAppBarDefaults.defaultScrollBehavior() as? MiuixGlassScrollBehavior
|
||||
val scrollBehavior = if (isMiuix) {
|
||||
MiuixGlassScrollBehavior(MiuixScrollBehavior())
|
||||
} else {
|
||||
null
|
||||
M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
|
||||
}
|
||||
val miuixCollapsedFraction = miuixScrollBehavior?.collapsedFraction ?: 0f
|
||||
val listState = rememberLazyListState()
|
||||
val colorThresholdPx = with(LocalDensity.current) { 6.dp.roundToPx() }
|
||||
val showAppBarColor by remember(listState, isMiuix, colorThresholdPx, miuixCollapsedFraction) {
|
||||
derivedStateOf {
|
||||
if (isMiuix) {
|
||||
miuixCollapsedFraction > 0.01f
|
||||
} else {
|
||||
listState.firstVisibleItemIndex > 0 ||
|
||||
listState.firstVisibleItemScrollOffset > colorThresholdPx
|
||||
}
|
||||
}
|
||||
}
|
||||
val appBarColor by animateColorAsState(
|
||||
targetValue = if (showAppBarColor) {
|
||||
LegadoTheme.colorScheme.surfaceContainer
|
||||
} else {
|
||||
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0f)
|
||||
},
|
||||
label = "book-info-top-bar-color",
|
||||
)
|
||||
val pullState = rememberPullToRefreshState()
|
||||
var showMenu by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@@ -196,63 +185,16 @@ private fun BookInfoScreenContent(
|
||||
AppScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(
|
||||
if (miuixScrollBehavior != null) {
|
||||
Modifier.nestedScroll(miuixScrollBehavior.nestedScrollConnection)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
if (isMiuix) {
|
||||
MiuixTopAppBar(
|
||||
title = "",
|
||||
color = appBarColor,
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(
|
||||
onClick = { onIntent(BookInfoIntent.BackPressed) }
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
BookInfoTopBarActions(
|
||||
state = state,
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = { showMenu = it },
|
||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
||||
)
|
||||
},
|
||||
scrollBehavior = miuixScrollBehavior?.miuixBehavior
|
||||
)
|
||||
} else {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "",
|
||||
maxLines = 1,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(
|
||||
onClick = { onIntent(BookInfoIntent.BackPressed) }
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
BookInfoTopBarActions(
|
||||
state = state,
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = { showMenu = it },
|
||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = appBarColor,
|
||||
scrolledContainerColor = appBarColor,
|
||||
navigationIconContentColor = LegadoTheme.colorScheme.onSurface,
|
||||
titleContentColor = LegadoTheme.colorScheme.onSurface,
|
||||
actionIconContentColor = LegadoTheme.colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
}
|
||||
BookInfoTransparentTopAppBar(
|
||||
state = state,
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = { showMenu = it },
|
||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
||||
onBackPressed = { onIntent(BookInfoIntent.BackPressed) },
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
@@ -322,6 +264,7 @@ private fun BookInfoScreenContent(
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
BookInfoActions(
|
||||
book = book,
|
||||
inBookshelf = state.inBookshelf,
|
||||
onShelfClick = { onIntent(BookInfoIntent.ShelfClick) },
|
||||
onTocClick = { onIntent(BookInfoIntent.TocClick) },
|
||||
@@ -416,6 +359,70 @@ private fun BookInfoColorTheme(
|
||||
ProvideThemeOverride(theme = theme, content = content)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
private fun BookInfoTransparentTopAppBar(
|
||||
state: BookInfoUiState,
|
||||
showMenu: Boolean,
|
||||
onShowMenuChange: (Boolean) -> Unit,
|
||||
onMenuAction: (BookInfoMenuAction) -> Unit,
|
||||
onBackPressed: () -> Unit,
|
||||
scrollBehavior: GlassTopAppBarScrollBehavior,
|
||||
) {
|
||||
val hazeState = LocalHazeState.current
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
val collapsedColor = if (isMiuix) {
|
||||
GlassTopAppBarDefaults.getMiuixAppBarColor()
|
||||
} else {
|
||||
GlassTopAppBarDefaults.scrolledContainerColor()
|
||||
}
|
||||
val isAtTop = scrollBehavior.collapsedFraction <= 0.001f
|
||||
val resolvedColor = if (isAtTop) Color.Transparent else collapsedColor
|
||||
val topBarColors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = resolvedColor,
|
||||
scrolledContainerColor = resolvedColor,
|
||||
)
|
||||
|
||||
if (isMiuix) {
|
||||
MiuixTopAppBar(
|
||||
modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier,
|
||||
title = "",
|
||||
subtitle = "",
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackPressed)
|
||||
},
|
||||
actions = {
|
||||
BookInfoTopBarActions(
|
||||
state = state,
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = onShowMenuChange,
|
||||
onMenuAction = onMenuAction,
|
||||
)
|
||||
},
|
||||
color = resolvedColor,
|
||||
scrollBehavior = (scrollBehavior as? MiuixGlassScrollBehavior)?.miuixBehavior,
|
||||
)
|
||||
} else {
|
||||
MediumFlexibleTopAppBar(
|
||||
modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier,
|
||||
title = { Text(text = "", maxLines = 1) },
|
||||
navigationIcon = {
|
||||
TopBarNavigationButton(onClick = onBackPressed)
|
||||
},
|
||||
actions = {
|
||||
BookInfoTopBarActions(
|
||||
state = state,
|
||||
showMenu = showMenu,
|
||||
onShowMenuChange = onShowMenuChange,
|
||||
onMenuAction = onMenuAction,
|
||||
)
|
||||
},
|
||||
scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
|
||||
colors = topBarColors,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
@@ -470,6 +477,19 @@ private fun BookInfoTopBarActions(
|
||||
@Composable
|
||||
private fun BookInfoBackdrop(book: Book) {
|
||||
val cover = book.getDisplayCover()
|
||||
val sourceOrigin = book.origin
|
||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||
val context = LocalContext.current
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) {
|
||||
buildCoverImageRequest(
|
||||
context = context,
|
||||
data = cover,
|
||||
sourceOrigin = sourceOrigin,
|
||||
loadOnlyWifi = loadOnlyWifi,
|
||||
crossfade = false,
|
||||
)
|
||||
}
|
||||
val seedOverlay = lerp(
|
||||
LegadoTheme.colorScheme.secondaryContainer,
|
||||
LegadoTheme.seedColor,
|
||||
@@ -478,7 +498,8 @@ private fun BookInfoBackdrop(book: Book) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (!cover.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = cover,
|
||||
model = backdropRequest,
|
||||
imageLoader = imageLoader,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -567,22 +588,24 @@ private fun BookInfoOverflowMenu(
|
||||
text = stringResource(R.string.to_top),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.Top) }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.allow_update),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
|
||||
trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) }
|
||||
)
|
||||
if (book?.isLocal == true && book.type and io.legado.app.constant.BookType.text > 0) {
|
||||
if (book?.isLocal == false ){
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.allow_update),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
|
||||
isSelected = book.canUpdate
|
||||
)
|
||||
}
|
||||
if (book?.isLocal == true && book.type and BookType.text > 0) {
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.split_long_chapter),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
|
||||
trailingIcon = { if (book.getSplitLongChapter()) Icon(Icons.Default.Star, null) }
|
||||
isSelected = book.getSplitLongChapter()
|
||||
)
|
||||
}
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.delete_alert),
|
||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
|
||||
trailingIcon = { if (io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert) Icon(Icons.Default.Star, null) }
|
||||
isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.clear_cache),
|
||||
@@ -606,11 +629,6 @@ private fun BookInfoHeader(
|
||||
onBookNameClick: (Boolean) -> Unit,
|
||||
onOriginClick: () -> Unit,
|
||||
) {
|
||||
val labelBackground = lerp(
|
||||
LegadoTheme.colorScheme.surface,
|
||||
LegadoTheme.seedColor,
|
||||
0.1f
|
||||
).copy(alpha = 0.82f)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -688,6 +706,16 @@ private fun BookInfoHeader(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
groupNames?.takeIf { it.isNotBlank() }?.let {
|
||||
item(key = "group-$it") {
|
||||
TextCard(
|
||||
text = stringResource(R.string.group_s, it),
|
||||
textStyle = LegadoTheme.typography.labelLargeEmphasized,
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(
|
||||
items = kindLabels,
|
||||
key = { index, label -> "kind-$index-$label" }
|
||||
@@ -695,20 +723,10 @@ private fun BookInfoHeader(
|
||||
TextCard(
|
||||
text = label,
|
||||
textStyle = LegadoTheme.typography.labelLargeEmphasized,
|
||||
backgroundColor = labelBackground,
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
groupNames?.takeIf { it.isNotBlank() }?.let {
|
||||
item(key = "group-$it") {
|
||||
TextCard(
|
||||
text = stringResource(R.string.group_s, it),
|
||||
textStyle = LegadoTheme.typography.labelLargeEmphasized,
|
||||
backgroundColor = labelBackground,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -717,6 +735,7 @@ private fun BookInfoHeader(
|
||||
|
||||
@Composable
|
||||
private fun BookInfoActions(
|
||||
book: Book,
|
||||
inBookshelf: Boolean,
|
||||
onShelfClick: () -> Unit,
|
||||
onTocClick: () -> Unit,
|
||||
@@ -747,7 +766,7 @@ private fun BookInfoActions(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(LegadoTheme.colorScheme.surface)
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
BookInfoActionCard(
|
||||
@@ -772,7 +791,7 @@ private fun BookInfoActions(
|
||||
)
|
||||
BookInfoActionCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
icon = Icons.Default.Bookmark,
|
||||
icon = if (book.group > 0) Icons.Default.Bookmark else Icons.Outlined.Bookmark,
|
||||
label = stringResource(R.string.change_group),
|
||||
onClick = onGroupClick
|
||||
)
|
||||
@@ -788,14 +807,14 @@ private fun BookInfoActions(
|
||||
@Composable
|
||||
private fun BookInfoActionCard(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = modifier,
|
||||
onClick = onClick,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.2f),
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
) {
|
||||
Column(
|
||||
|
||||
@@ -9,6 +9,7 @@ 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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
@@ -65,6 +66,7 @@ import io.legado.app.ui.book.group.GroupEditSheet
|
||||
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.AppLinearProgressIndicator
|
||||
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.ConfirmDismissButtonsRow
|
||||
@@ -134,53 +136,58 @@ fun GroupSelectSheet(
|
||||
title = stringResource(R.string.group_select),
|
||||
endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } }
|
||||
) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(groups, key = { it.groupId }) { group ->
|
||||
val isSelected = selectedGroupId and group.groupId > 0
|
||||
SelectionItemCard(
|
||||
title = group.groupName,
|
||||
isSelected = isSelected,
|
||||
onToggleSelection = {
|
||||
selectedGroupId = if (isSelected) {
|
||||
selectedGroupId - group.groupId
|
||||
} else {
|
||||
selectedGroupId + group.groupId
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
selectedGroupId = if (it) {
|
||||
selectedGroupId + group.groupId
|
||||
} else {
|
||||
selectedGroupId - group.groupId
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 600.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(groups, key = { it.groupId }) { group ->
|
||||
val isSelected = selectedGroupId and group.groupId > 0
|
||||
SelectionItemCard(
|
||||
title = group.groupName,
|
||||
isSelected = isSelected,
|
||||
onToggleSelection = {
|
||||
selectedGroupId = if (isSelected) {
|
||||
selectedGroupId - group.groupId
|
||||
} else {
|
||||
selectedGroupId + group.groupId
|
||||
}
|
||||
)
|
||||
},
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { editingGroup = group },
|
||||
imageVector = Icons.Default.Edit
|
||||
)
|
||||
},
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = {
|
||||
selectedGroupId = if (it) {
|
||||
selectedGroupId + group.groupId
|
||||
} else {
|
||||
selectedGroupId - group.groupId
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { editingGroup = group },
|
||||
imageVector = Icons.Default.Edit
|
||||
)
|
||||
},
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
ConfirmDismissButtonsRow(
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = { onConfirm(selectedGroupId) },
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
ConfirmDismissButtonsRow(
|
||||
onDismiss = onDismissRequest,
|
||||
onConfirm = { onConfirm(selectedGroupId) },
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ChangeCoverSheet(
|
||||
@@ -376,7 +383,7 @@ fun ChangeSourceSheet(
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
if (isSearching) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppText(
|
||||
text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}",
|
||||
|
||||
@@ -69,12 +69,11 @@ import cn.hutool.core.date.DateUtil
|
||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.CollapsibleHeader
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SearchBarSection
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AppIconButton
|
||||
@@ -248,7 +247,7 @@ fun ReadRecordScreen(
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
"LOADING" -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
@@ -261,7 +260,7 @@ fun ReadRecordScreen(
|
||||
}
|
||||
|
||||
"EMPTY" -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
|
||||
@@ -1,674 +1,73 @@
|
||||
package io.legado.app.ui.book.search
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.material.search.SearchBar
|
||||
import com.google.android.material.search.SearchView
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import io.legado.app.databinding.ActivityBookSearchBinding
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.model.BookShelfState
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||
import io.legado.app.utils.applyNavigationBarMargin
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.transaction
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import kotlin.math.abs
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel>(),
|
||||
BookAdapter.CallBack,
|
||||
HistoryKeyAdapter.CallBack,
|
||||
SearchScopeDialog.Callback,
|
||||
SearchAdapter.CallBack {
|
||||
class SearchActivity : BaseComposeActivity() {
|
||||
|
||||
override val binding by viewBinding(ActivityBookSearchBinding::inflate)
|
||||
override val viewModel by viewModels<SearchViewModel>()
|
||||
|
||||
private val adapter by lazy { SearchAdapter(this, this) }
|
||||
private val bookAdapter by lazy {
|
||||
BookAdapter(this, this).apply {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
}
|
||||
private val historyKeyAdapter by lazy {
|
||||
HistoryKeyAdapter(this, this).apply {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val searchBar: SearchBar by lazy { binding.searchBar }
|
||||
private val searchView: SearchView by lazy { binding.searchView }
|
||||
|
||||
private var menu: Menu? = null
|
||||
private var groups: List<String>? = null
|
||||
private var historyFlowJob: Job? = null
|
||||
private var booksFlowJob: Job? = null
|
||||
private var precisionSearchMenuItem: MenuItem? = null
|
||||
private var isManualStopSearch = false
|
||||
private val viewModel by viewModel<SearchViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setSupportActionBar(binding.searchBar)
|
||||
initRecyclerView()
|
||||
initMaterialSearch()
|
||||
initOtherView()
|
||||
initData()
|
||||
updateSelectedGroup()
|
||||
receiptIntent(intent)
|
||||
dispatchInit(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
receiptIntent(intent)
|
||||
setIntent(intent)
|
||||
dispatchInit(intent)
|
||||
}
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.book_search, menu)
|
||||
this.menu = menu
|
||||
precisionSearchMenuItem = menu.findItem(R.id.menu_precision_search)
|
||||
precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.onIntent(SearchIntent.ResumeEngine)
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
buildSearchScopeMenu(menu)
|
||||
updateSearchScopeChip()
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
override fun onPause() {
|
||||
viewModel.onIntent(SearchIntent.PauseEngine)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_precision_search -> {
|
||||
val newValue = !getPrefBoolean(PreferKey.precisionSearch)
|
||||
putPrefBoolean(PreferKey.precisionSearch, newValue)
|
||||
precisionSearchMenuItem?.isChecked = newValue
|
||||
binding.chipPrecisionSearch.isChecked = newValue
|
||||
binding.searchView.text.toString().trim().let {
|
||||
binding.searchView.setText(it)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
SearchScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { finish() },
|
||||
onOpenBookInfo = { name, author, bookUrl ->
|
||||
startActivity<BookInfoActivity> {
|
||||
putExtra("name", name)
|
||||
putExtra("author", author)
|
||||
putExtra("bookUrl", bookUrl)
|
||||
}
|
||||
},
|
||||
onOpenSourceManage = {
|
||||
startActivity<BookSourceActivity>()
|
||||
}
|
||||
|
||||
R.id.menu_search_scope -> alertSearchScope()
|
||||
R.id.menu_source_manage -> startActivity<BookSourceActivity>()
|
||||
R.id.menu_log -> showDialogFragment(AppLogDialog())
|
||||
R.id.menu_1 -> {
|
||||
viewModel.searchScope.update("")
|
||||
updateSelectedGroup()
|
||||
}
|
||||
else -> {
|
||||
if (item.groupId == R.id.menu_group_1) {
|
||||
viewModel.searchScope.remove(item.title.toString())
|
||||
} else if (item.groupId == R.id.menu_group_2) {
|
||||
viewModel.searchScope.update(item.title.toString())
|
||||
}
|
||||
updateSelectedGroup()
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
)
|
||||
}
|
||||
|
||||
private fun initMaterialSearch() {
|
||||
binding.chipPrecisionSearch.isChecked = getPrefBoolean(PreferKey.precisionSearch)
|
||||
binding.chipPrecisionSearch.setOnCheckedChangeListener { chip, isChecked ->
|
||||
putPrefBoolean(PreferKey.precisionSearch, isChecked)
|
||||
precisionSearchMenuItem?.isChecked = isChecked
|
||||
binding.searchView.text.toString().trim().let {
|
||||
binding.searchView.setText(it)
|
||||
}
|
||||
}
|
||||
binding.chipSearchScope.setOnClickListener {
|
||||
alertSearchScope()
|
||||
}
|
||||
binding.chipGroup.setOnClickListener { view ->
|
||||
binding.chipGroup.isChecked = true
|
||||
val popup = PopupMenu(this, view)
|
||||
val menu = popup.menu
|
||||
buildSearchScopeMenu(menu)
|
||||
updateSearchScopeChip()
|
||||
var fromGroup1 = false
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.groupId) {
|
||||
R.id.menu_group_1 -> {
|
||||
viewModel.searchScope.remove(item.title.toString())
|
||||
fromGroup1 = true
|
||||
}
|
||||
R.id.menu_group_2 -> {
|
||||
if (item.itemId == R.id.menu_1) {
|
||||
viewModel.searchScope.update("")
|
||||
fromGroup1 = true
|
||||
} else {
|
||||
viewModel.searchScope.update(item.title.toString())
|
||||
fromGroup1 = false
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
viewModel.searchScope.update(item.title.toString())
|
||||
fromGroup1 = false
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedGroup()
|
||||
true
|
||||
}
|
||||
popup.setOnDismissListener {
|
||||
binding.chipGroup.isChecked = !fromGroup1
|
||||
updateSearchScopeChip()
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
|
||||
searchBar.setOnMenuItemClickListener { item ->
|
||||
onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
searchBar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
searchView.setupWithSearchBar(searchBar)
|
||||
|
||||
binding.searchView.show()
|
||||
binding.searchView.editText.requestFocus()
|
||||
|
||||
searchView.editText.hint = getString(R.string.search_book_key)
|
||||
searchView.editText.setOnEditorActionListener { _, actionId, _ ->
|
||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
||||
doSearch()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
searchView.editText.setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
|
||||
doSearch()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
searchView.editText.doAfterTextChanged {
|
||||
viewModel.stop()
|
||||
binding.fbStartStop.invisible()
|
||||
upHistory(it?.toString()?.trim().orEmpty())
|
||||
}
|
||||
|
||||
searchView.addTransitionListener { _, _, newState ->
|
||||
if (newState == SearchView.TransitionState.HIDDEN) {
|
||||
searchBar.setText(searchView.text)
|
||||
}
|
||||
}
|
||||
|
||||
searchView.addTransitionListener { _, oldState, newState ->
|
||||
if (oldState == SearchView.TransitionState.HIDDEN && newState == SearchView.TransitionState.SHOWN) {
|
||||
searchView.editText.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
visibleInputHelp()
|
||||
}
|
||||
|
||||
private fun doSearch() {
|
||||
val key = searchView.text.toString().trim()
|
||||
if (key.isNotEmpty()) {
|
||||
isManualStopSearch = false
|
||||
viewModel.saveSearchKey(key)
|
||||
viewModel.searchKey = ""
|
||||
viewModel.search(key)
|
||||
visibleInputHelp()
|
||||
searchView.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectedGroup() {
|
||||
val displayNames = viewModel.searchScope.displayNames
|
||||
val groupName = displayNames.firstOrNull().orEmpty()
|
||||
binding.chipGroup.text = groupName.ifEmpty { getString(R.string.search_select_group) }
|
||||
binding.chipGroup.isChecked = groupName.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun buildSearchScopeMenu(menu: Menu) {
|
||||
menu.transaction {
|
||||
menu.removeGroup(R.id.menu_group_1)
|
||||
menu.removeGroup(R.id.menu_group_2)
|
||||
var hasChecked = false
|
||||
val searchScopeNames = viewModel.searchScope.displayNames
|
||||
|
||||
if (viewModel.searchScope.isSource()) {
|
||||
menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, searchScopeNames.first()).apply {
|
||||
isChecked = true
|
||||
hasChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
val allSourceMenu =
|
||||
menu.add(R.id.menu_group_2, R.id.menu_1, Menu.NONE, getString(R.string.all_source))
|
||||
.apply {
|
||||
if (searchScopeNames.isEmpty()) {
|
||||
isChecked = true
|
||||
hasChecked = true
|
||||
}
|
||||
}
|
||||
|
||||
groups?.forEach { groupName ->
|
||||
if (searchScopeNames.contains(groupName)) {
|
||||
menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, groupName).apply {
|
||||
isChecked = true
|
||||
hasChecked = true
|
||||
}
|
||||
} else {
|
||||
menu.add(R.id.menu_group_2, Menu.NONE, Menu.NONE, groupName)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasChecked) {
|
||||
viewModel.searchScope.update("")
|
||||
allSourceMenu.isChecked = true
|
||||
}
|
||||
|
||||
menu.setGroupCheckable(R.id.menu_group_1, true, false)
|
||||
menu.setGroupCheckable(R.id.menu_group_2, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initRecyclerView() {
|
||||
binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this)
|
||||
binding.rvBookshelfSearch.adapter = bookAdapter
|
||||
binding.rvBookshelfSearch.itemAnimator = DefaultItemAnimator()
|
||||
|
||||
binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this)
|
||||
binding.rvHistoryKey.adapter = historyKeyAdapter
|
||||
binding.rvHistoryKey.itemAnimator = DefaultItemAnimator()
|
||||
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.itemAnimator = DefaultItemAnimator()
|
||||
|
||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
if (positionStart == 0) {
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
||||
if (toPosition == 0) {
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
if (!recyclerView.canScrollVertically(1)) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
if (lastPosition == RecyclerView.NO_POSITION) {
|
||||
return
|
||||
}
|
||||
val lastView = layoutManager.findViewByPosition(lastPosition)
|
||||
if (lastView == null) {
|
||||
scrollToBottom()
|
||||
return
|
||||
}
|
||||
val bottom =
|
||||
abs(lastView.bottom - recyclerView.height) - recyclerView.paddingBottom
|
||||
if (bottom <= 1) {
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun initOtherView() {
|
||||
binding.llStop.applyNavigationBarMargin()
|
||||
binding.fbStartStop.setOnClickListener {
|
||||
if (viewModel.isSearchLiveData.value == true) {
|
||||
isManualStopSearch = true
|
||||
viewModel.stop()
|
||||
} else {
|
||||
viewModel.search("")
|
||||
}
|
||||
}
|
||||
binding.tvClearHistory.setOnClickListener { alertClearHistory() }
|
||||
}
|
||||
|
||||
private fun initData() {
|
||||
viewModel.searchScope.stateLiveData.observe(this) {
|
||||
if (!binding.llInputHelp.isVisible) {
|
||||
searchView.text.toString().trim().let {
|
||||
searchView.setText(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.isSearchLiveData.observe(this) {
|
||||
if (it) {
|
||||
startSearch()
|
||||
} else {
|
||||
searchFinally()
|
||||
}
|
||||
}
|
||||
viewModel.searchBookLiveData.observe(this) {
|
||||
adapter.setItems(it)
|
||||
if (it.isNullOrEmpty()) {
|
||||
binding.tvEmptyMsg.visible()
|
||||
} else {
|
||||
binding.tvEmptyMsg.gone()
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
appDb.bookSourceDao.flowEnabledGroups().collect {
|
||||
groups = it
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
viewModel.resume()
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
viewModel.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理传入数据
|
||||
*/
|
||||
private fun receiptIntent(intent: Intent? = null) {
|
||||
val searchScope = intent?.getStringExtra("searchScope")
|
||||
searchScope?.let {
|
||||
viewModel.searchScope.update(searchScope, false)
|
||||
updateSearchScopeChip()
|
||||
}
|
||||
val key = intent?.getStringExtra("key")
|
||||
if (key.isNullOrBlank()) {
|
||||
searchView.editText.requestFocus()
|
||||
} else {
|
||||
searchView.setText(key)
|
||||
searchView.postDelayed({doSearch()},500)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 滚动到底部事件
|
||||
*/
|
||||
private fun scrollToBottom() {
|
||||
if (isManualStopSearch) {
|
||||
return
|
||||
}
|
||||
if (viewModel.isSearchLiveData.value == false
|
||||
&& viewModel.searchKey.isNotEmpty()
|
||||
&& viewModel.hasMore
|
||||
) {
|
||||
viewModel.search("")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开关闭输入帮助
|
||||
*/
|
||||
private fun visibleInputHelp() {
|
||||
upHistory(searchView.text.toString())
|
||||
binding.llInputHelp.visibility = VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新搜索历史
|
||||
*/
|
||||
private fun upHistory(key: String? = null) {
|
||||
booksFlowJob?.cancel()
|
||||
booksFlowJob = lifecycleScope.launch {
|
||||
if (key.isNullOrBlank()) {
|
||||
binding.tvBookShow.gone()
|
||||
binding.rvBookshelfSearch.gone()
|
||||
} else {
|
||||
appDb.bookDao.flowSearch(key).conflate().collect {
|
||||
if (it.isEmpty()) {
|
||||
binding.tvBookShow.gone()
|
||||
binding.rvBookshelfSearch.gone()
|
||||
} else {
|
||||
binding.tvBookShow.visible()
|
||||
binding.rvBookshelfSearch.visible()
|
||||
}
|
||||
bookAdapter.setItems(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
historyFlowJob?.cancel()
|
||||
historyFlowJob = lifecycleScope.launch {
|
||||
when {
|
||||
key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime()
|
||||
else -> appDb.searchKeywordDao.flowSearch(key)
|
||||
}.catch {
|
||||
AppLog.put("搜索界面获取搜索历史数据失败\n${it.localizedMessage}", it)
|
||||
}.flowOn(IO).conflate().collect {
|
||||
historyKeyAdapter.setItems(it)
|
||||
if (it.isEmpty()) {
|
||||
binding.tvClearHistory.invisible()
|
||||
} else {
|
||||
binding.tvClearHistory.visible()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始搜索
|
||||
*/
|
||||
private fun startSearch() {
|
||||
binding.refreshProgressBar.visible()
|
||||
binding.cdProgress.visible()
|
||||
//binding.refreshProgressBar.isAutoLoading = true
|
||||
binding.fbStartStop.setImageResource(R.drawable.ic_stop_black_24dp)
|
||||
binding.fbStartStop.visible()
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索结束
|
||||
*/
|
||||
private fun searchFinally() {
|
||||
TransitionManager.beginDelayedTransition(binding.root)
|
||||
binding.refreshProgressBar.gone()
|
||||
binding.cdProgress.gone()
|
||||
if (!isManualStopSearch && viewModel.hasMore) {
|
||||
binding.fbStartStop.setImageResource(R.drawable.ic_play)
|
||||
} else {
|
||||
binding.fbStartStop.invisible()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun observeLiveBus() {
|
||||
viewModel.upAdapterLiveData.observe(this) {
|
||||
adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null))
|
||||
}
|
||||
viewModel.searchProgressLiveData.observe(this) { (processed, total) ->
|
||||
val progress = (processed * 100 / total).coerceAtMost(100)
|
||||
binding.refreshProgressBar.setProgress(progress, true)
|
||||
binding.tvProgress.text = "$processed / $total"
|
||||
}
|
||||
viewModel.searchFinishLiveData.observe(this) { isEmpty ->
|
||||
if (!isEmpty || viewModel.searchScope.isAll()) return@observe
|
||||
alert("搜索结果为空") {
|
||||
val precisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch)
|
||||
val displayScope = viewModel.searchScope.display
|
||||
if (precisionSearch) {
|
||||
setMessage("${displayScope}分组搜索结果为空,是否关闭精准搜索?")
|
||||
yesButton {
|
||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
|
||||
precisionSearchMenuItem?.isChecked = false
|
||||
viewModel.searchKey = ""
|
||||
viewModel.search(searchView.text.toString())
|
||||
doSearch()
|
||||
}
|
||||
} else {
|
||||
setMessage("${displayScope}分组搜索结果为空,是否切换到全部分组?")
|
||||
yesButton {
|
||||
viewModel.searchScope.update("")
|
||||
doSearch()
|
||||
}
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示书籍详情
|
||||
*/
|
||||
override fun showBookInfo(name: String, author: String, bookUrl: String) {
|
||||
startActivity<BookInfoActivity> {
|
||||
putExtra("name", name)
|
||||
putExtra("author", author)
|
||||
putExtra("bookUrl", bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已经加入书架
|
||||
*/
|
||||
override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState {
|
||||
return viewModel.isInBookShelf(name, author, url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示书籍详情
|
||||
*/
|
||||
override fun showBookInfo(book: Book) {
|
||||
showBookInfo(book.name, book.author, book.bookUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击历史关键字
|
||||
*/
|
||||
override fun searchHistory(key: String) {
|
||||
lifecycleScope.launch {
|
||||
when {
|
||||
searchView.text.toString() == key -> {
|
||||
searchView.setText(key)
|
||||
}
|
||||
|
||||
withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> {
|
||||
searchView.setText(key)
|
||||
}
|
||||
|
||||
else -> {
|
||||
searchView.setText(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除搜索记录
|
||||
*/
|
||||
override fun deleteHistory(searchKeyword: SearchKeyword) {
|
||||
viewModel.deleteHistory(searchKeyword)
|
||||
}
|
||||
|
||||
|
||||
override fun onSearchScopeOk(searchScope: SearchScope) {
|
||||
viewModel.searchScope.update(searchScope.toString())
|
||||
updateSearchScopeChip()
|
||||
}
|
||||
|
||||
private fun updateSearchScopeChip() {
|
||||
binding.chipSearchScope.isCheckable = true
|
||||
val scopeNames = viewModel.searchScope.displayNames
|
||||
if (scopeNames.isEmpty() || viewModel.searchScope.isAll()) {
|
||||
binding.chipSearchScope.apply {
|
||||
isChecked = false
|
||||
text = getString(R.string.all_source)
|
||||
}
|
||||
} else {
|
||||
binding.chipSearchScope.apply {
|
||||
isChecked = true
|
||||
text = scopeNames.joinToString(", ")
|
||||
}
|
||||
}
|
||||
binding.chipSearchScope.isCheckable = false
|
||||
}
|
||||
|
||||
private fun alertSearchScope() {
|
||||
showDialogFragment<SearchScopeDialog>()
|
||||
}
|
||||
|
||||
private fun alertClearHistory() {
|
||||
alert(R.string.draw) {
|
||||
setMessage(R.string.sure_clear_search_history)
|
||||
yesButton {
|
||||
viewModel.clearHistory()
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
if (searchView.hasFocus()) {
|
||||
searchView.clearFocus()
|
||||
}
|
||||
super.finish()
|
||||
private fun dispatchInit(intent: Intent?) {
|
||||
viewModel.onIntent(
|
||||
SearchIntent.Initialize(
|
||||
key = intent?.getStringExtra("key"),
|
||||
scopeRaw = intent?.getStringExtra("searchScope"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun start(context: Context, key: String?, searchScope: String? = null) {
|
||||
context.startActivity<SearchActivity> {
|
||||
putExtra("key", key)
|
||||
putExtra("searchScope", searchScope)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,11 @@ data class SearchScope(private var scope: String) {
|
||||
constructor(groups: List<String>) : this(groups.joinToString(","))
|
||||
|
||||
constructor(source: BookSource) : this(
|
||||
"${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
)
|
||||
|
||||
constructor(source: BookSourcePart) : this(
|
||||
"${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}"
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
@@ -44,19 +44,35 @@ data class SearchScope(private var scope: String) {
|
||||
}
|
||||
|
||||
fun update(source: BookSource) {
|
||||
scope = "${source.bookSourceName}::${source.bookSourceUrl}"
|
||||
scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun update(source: BookSourcePart) {
|
||||
scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun updateSources(sources: List<BookSourcePart>) {
|
||||
scope = encodeSourceScope(sources)
|
||||
stateLiveData.postValue(scope)
|
||||
save()
|
||||
}
|
||||
|
||||
fun isSource(): Boolean {
|
||||
return scope.contains("::")
|
||||
val items = scopeItems()
|
||||
if (items.isEmpty()) return false
|
||||
return parseSourceItems(items).size == items.size
|
||||
}
|
||||
|
||||
val display: String
|
||||
get() {
|
||||
if (scope.contains("::")) {
|
||||
return scope.substringBefore("::")
|
||||
if (isSource()) {
|
||||
val sourceNames = parseSourceItems().map { it.name }
|
||||
if (sourceNames.isEmpty()) return appCtx.getString(R.string.all_source)
|
||||
return sourceNames.joinToString(",")
|
||||
}
|
||||
if (scope.isEmpty()) {
|
||||
return appCtx.getString(R.string.all_source)
|
||||
@@ -70,22 +86,30 @@ data class SearchScope(private var scope: String) {
|
||||
val displayNames: List<String>
|
||||
get() {
|
||||
val list = arrayListOf<String>()
|
||||
if (scope.contains("::")) {
|
||||
list.add(scope.substringBefore("::"))
|
||||
if (isSource()) {
|
||||
parseSourceItems().forEach {
|
||||
list.add(it.name)
|
||||
}
|
||||
} else {
|
||||
scope.splitNotBlank(",").forEach {
|
||||
scopeItems().forEach {
|
||||
list.add(it)
|
||||
}
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
val sourceUrls: List<String>
|
||||
get() = parseSourceItems().map { it.url }
|
||||
|
||||
fun remove(scope: String) {
|
||||
if (isSource()) {
|
||||
this.scope = ""
|
||||
val sourceItems = parseSourceItems().filterNot {
|
||||
it.name == scope || it.url == scope
|
||||
}
|
||||
this.scope = sourceItems.joinToString(",") { "${it.name}::${it.url}" }
|
||||
} else {
|
||||
val stringBuilder = StringBuilder()
|
||||
this.scope.split(",").forEach {
|
||||
scopeItems().forEach {
|
||||
if (it != scope) {
|
||||
if (stringBuilder.isNotEmpty()) {
|
||||
stringBuilder.append(",")
|
||||
@@ -106,14 +130,14 @@ data class SearchScope(private var scope: String) {
|
||||
if (scope.isEmpty()) {
|
||||
list.addAll(appDb.bookSourceDao.allEnabledPart)
|
||||
} else {
|
||||
if (scope.contains("::")) {
|
||||
scope.substringAfter("::").let {
|
||||
appDb.bookSourceDao.getBookSourcePart(it)?.let { source ->
|
||||
if (isSource()) {
|
||||
parseSourceItems().forEach { sourceItem ->
|
||||
appDb.bookSourceDao.getBookSourcePart(sourceItem.url)?.let { source ->
|
||||
list.add(source)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val oldScope = scope.splitNotBlank(",")
|
||||
val oldScope = scopeItems()
|
||||
val newScope = oldScope.filter {
|
||||
val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it)
|
||||
list.addAll(bookSources)
|
||||
@@ -150,4 +174,43 @@ data class SearchScope(private var scope: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun scopeItems(): List<String> = scope.splitNotBlank(",").toList()
|
||||
|
||||
private fun parseSourceItems(
|
||||
items: List<String> = scopeItems()
|
||||
): List<ScopeSourceItem> {
|
||||
return items.mapNotNull { item ->
|
||||
val splitIndex = item.indexOf("::")
|
||||
if (splitIndex <= 0 || splitIndex >= item.lastIndex) {
|
||||
null
|
||||
} else {
|
||||
ScopeSourceItem(
|
||||
name = item.substring(0, splitIndex),
|
||||
url = item.substring(splitIndex + 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class ScopeSourceItem(
|
||||
val name: String,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun sanitizeSourceName(name: String): String {
|
||||
return name.replace(":", "").replace(",", "")
|
||||
}
|
||||
|
||||
private fun encodeSourceToken(name: String, url: String): String {
|
||||
return "${sanitizeSourceName(name)}::${url}"
|
||||
}
|
||||
|
||||
private fun encodeSourceScope(sources: List<BookSourcePart>): String {
|
||||
return sources.joinToString(",") { source ->
|
||||
encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,166 +1,423 @@
|
||||
package io.legado.app.ui.book.search
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.entities.BookSourcePart
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import io.legado.app.help.book.isNotShelf
|
||||
import io.legado.app.data.repository.SearchRepository
|
||||
import io.legado.app.data.repository.SearchSessionEvent
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.BookShelfState
|
||||
import io.legado.app.model.webBook.SearchModel
|
||||
import io.legado.app.utils.ConflateLiveData
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.ui.main.bookshelf.BookShelfItem
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
data class BookKey(val name: String, val author: String, val url: String?)
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.init.appCtx
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SearchViewModel(application: Application) : BaseViewModel(application) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val bookshelf: MutableSet<BookKey> = ConcurrentHashMap.newKeySet()
|
||||
val upAdapterLiveData = MutableLiveData<String>()
|
||||
var searchBookLiveData = ConflateLiveData<List<SearchBook>>(1000)
|
||||
val searchScope: SearchScope = SearchScope(AppConfig.searchScope)
|
||||
var searchFinishLiveData = MutableLiveData<Boolean>()
|
||||
var isSearchLiveData = MutableLiveData<Boolean>()
|
||||
class SearchViewModel(
|
||||
private val repository: SearchRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
val searchProgressLiveData = MutableLiveData<Pair<Int, Int>>()
|
||||
private val _uiState = MutableStateFlow(
|
||||
SearchUiState(
|
||||
isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch),
|
||||
scopeDisplay = SearchScope(AppConfig.searchScope).display,
|
||||
scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames,
|
||||
isAllScope = SearchScope(AppConfig.searchScope).isAll(),
|
||||
isSourceScope = SearchScope(AppConfig.searchScope).isSource(),
|
||||
)
|
||||
)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
var searchKey: String = ""
|
||||
var hasMore = true
|
||||
private var searchID = 0L
|
||||
private val searchModel = SearchModel(viewModelScope, object : SearchModel.CallBack {
|
||||
private val _effects = MutableSharedFlow<SearchEffect>(extraBufferCapacity = 16)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
override fun getSearchScope(): SearchScope {
|
||||
return searchScope
|
||||
}
|
||||
private val queryFlow = MutableStateFlow("")
|
||||
private val bookshelfKeys = MutableStateFlow<Set<BookKey>>(emptySet())
|
||||
private val searchScope = SearchScope(AppConfig.searchScope)
|
||||
private val searchSession = repository.createSearchSession { searchScope }
|
||||
|
||||
override fun onSearchStart() {
|
||||
isSearchLiveData.postValue(true)
|
||||
}
|
||||
|
||||
override fun onSearchSuccess(
|
||||
searchBooks: List<SearchBook>, processedSources: Int, totalSources: Int
|
||||
) {
|
||||
searchBookLiveData.postValue(searchBooks)
|
||||
searchProgressLiveData.postValue(Pair(processedSources, totalSources))
|
||||
}
|
||||
|
||||
override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) {
|
||||
this@SearchViewModel.hasMore = hasMore
|
||||
isSearchLiveData.postValue(false)
|
||||
searchFinishLiveData.postValue(isEmpty)
|
||||
}
|
||||
|
||||
override fun onSearchCancel(exception: Throwable?) {
|
||||
isSearchLiveData.postValue(false)
|
||||
exception?.let {
|
||||
context.toastOnUi(it.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
private var currentSearchId = 0L
|
||||
|
||||
init {
|
||||
execute {
|
||||
appDb.bookDao.flowAll().mapLatest { books ->
|
||||
books.filterNot { it.isNotShelf }
|
||||
}.catch {
|
||||
AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it)
|
||||
}.collect { books ->
|
||||
bookshelf.clear()
|
||||
bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) })
|
||||
upAdapterLiveData.postValue("isInBookshelf")
|
||||
syncScopeState()
|
||||
observeEnabledGroups()
|
||||
observeEnabledSources()
|
||||
observeBookshelf()
|
||||
observeQueryHistory()
|
||||
observeQueryBookshelfHints()
|
||||
observeSearchSession()
|
||||
}
|
||||
|
||||
fun onIntent(intent: SearchIntent) {
|
||||
when (intent) {
|
||||
is SearchIntent.Initialize -> initialize(intent.key, intent.scopeRaw)
|
||||
is SearchIntent.UpdateQuery -> updateQuery(intent.query, showSuggestions = true)
|
||||
SearchIntent.SubmitSearch -> submitSearch()
|
||||
SearchIntent.LoadMore -> loadMore()
|
||||
SearchIntent.StopSearch -> stopSearch()
|
||||
SearchIntent.PauseEngine -> searchSession.pause()
|
||||
SearchIntent.ResumeEngine -> searchSession.resume()
|
||||
is SearchIntent.UseHistoryKeyword -> {
|
||||
updateQuery(intent.keyword, showSuggestions = false)
|
||||
submitSearch(intent.keyword)
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("加载书架数据失败", it)
|
||||
|
||||
is SearchIntent.OpenSearchBook -> {
|
||||
emitEffect(
|
||||
SearchEffect.OpenBookInfo(
|
||||
name = intent.book.name,
|
||||
author = intent.book.author,
|
||||
bookUrl = intent.book.bookUrl,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is SearchIntent.OpenBookshelfBook -> {
|
||||
emitEffect(
|
||||
SearchEffect.OpenBookInfo(
|
||||
name = intent.book.name,
|
||||
author = intent.book.author,
|
||||
bookUrl = intent.book.bookUrl,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is SearchIntent.DeleteHistory -> viewModelScope.launch {
|
||||
repository.deleteSearchKeyword(intent.item)
|
||||
}
|
||||
|
||||
is SearchIntent.SetClearHistoryDialogVisible -> {
|
||||
_uiState.update { it.copy(showClearHistoryDialog = intent.visible) }
|
||||
}
|
||||
|
||||
SearchIntent.ConfirmClearHistory -> {
|
||||
_uiState.update { it.copy(showClearHistoryDialog = false) }
|
||||
viewModelScope.launch {
|
||||
repository.clearSearchKeywords()
|
||||
}
|
||||
}
|
||||
|
||||
is SearchIntent.SetScopeSheetVisible -> {
|
||||
_uiState.update { it.copy(showScopeSheet = intent.visible) }
|
||||
}
|
||||
|
||||
SearchIntent.SelectAllScope -> {
|
||||
searchScope.update("")
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
|
||||
is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
|
||||
is SearchIntent.RemoveScopeItem -> {
|
||||
searchScope.remove(intent.scopeName)
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
is SearchIntent.TogglePrecision -> {
|
||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled)
|
||||
_uiState.update { it.copy(isPrecisionSearch = intent.enabled) }
|
||||
restartCommittedSearchIfNeeded()
|
||||
}
|
||||
|
||||
SearchIntent.ConfirmEmptyScopeAction -> handleEmptyScopeActionConfirmed()
|
||||
SearchIntent.DismissEmptyScopeAction -> {
|
||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||
}
|
||||
|
||||
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInBookShelf(name: String, author: String, url: String?): BookShelfState {
|
||||
val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url }
|
||||
fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
|
||||
return bookshelfKeys
|
||||
.map { keys -> resolveBookShelfState(item, keys) }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
searchSession.close()
|
||||
}
|
||||
|
||||
private fun initialize(key: String?, scopeRaw: String?) {
|
||||
scopeRaw?.let {
|
||||
searchScope.update(it, postValue = false)
|
||||
}
|
||||
syncScopeState()
|
||||
|
||||
val initKey = key?.trim().orEmpty()
|
||||
if (initKey.isNotEmpty()) {
|
||||
updateQuery(initKey, showSuggestions = false)
|
||||
submitSearch(initKey)
|
||||
} else if (key != null) {
|
||||
updateQuery(initKey, showSuggestions = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEnabledGroups() {
|
||||
viewModelScope.launch {
|
||||
repository.enabledGroups
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { groups ->
|
||||
_uiState.update { it.copy(enabledGroups = groups) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeEnabledSources() {
|
||||
viewModelScope.launch {
|
||||
repository.enabledSources
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { sources ->
|
||||
_uiState.update { it.copy(enabledSources = sources) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeBookshelf() {
|
||||
viewModelScope.launch {
|
||||
repository.bookshelfKeys
|
||||
.catch { emit(emptySet()) }
|
||||
.collect { keys ->
|
||||
bookshelfKeys.value = keys
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeQueryHistory() {
|
||||
viewModelScope.launch {
|
||||
queryFlow
|
||||
.map { it.trim() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { repository.searchHistory(it) }
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { history ->
|
||||
_uiState.update { it.copy(history = history) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeQueryBookshelfHints() {
|
||||
viewModelScope.launch {
|
||||
queryFlow
|
||||
.map { it.trim() }
|
||||
.distinctUntilChanged()
|
||||
.flatMapLatest { repository.searchBookshelf(it) }
|
||||
.catch { emit(emptyList()) }
|
||||
.collect { books ->
|
||||
_uiState.update { it.copy(bookshelfHints = books) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeSearchSession() {
|
||||
viewModelScope.launch {
|
||||
searchSession.events.collect { event ->
|
||||
when (event) {
|
||||
SearchSessionEvent.Started -> {
|
||||
_uiState.update { it.copy(isSearching = true) }
|
||||
}
|
||||
|
||||
is SearchSessionEvent.Progress -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
results = event.books,
|
||||
processedSources = event.processedSources,
|
||||
totalSources = event.totalSources,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SearchSessionEvent.Finished -> {
|
||||
_uiState.update { state ->
|
||||
val emptyAction = if (event.isEmpty && !searchScope.isAll()) {
|
||||
SearchEmptyScopeAction(
|
||||
scopeDisplay = searchScope.display,
|
||||
wasPrecisionSearch = state.isPrecisionSearch,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
state.copy(
|
||||
isSearching = false,
|
||||
hasMore = event.hasMore,
|
||||
emptyScopeAction = emptyAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is SearchSessionEvent.Canceled -> {
|
||||
_uiState.update { it.copy(isSearching = false) }
|
||||
event.throwable?.localizedMessage
|
||||
?.takeIf { it.isNotBlank() }
|
||||
?.let { emitEffect(SearchEffect.ShowMessage(it)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateQuery(query: String, showSuggestions: Boolean) {
|
||||
if (showSuggestions && _uiState.value.isSearching) {
|
||||
searchSession.stop()
|
||||
}
|
||||
queryFlow.value = query
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
query = query,
|
||||
showSuggestions = showSuggestions,
|
||||
isManualStop = false,
|
||||
emptyScopeAction = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitSearch(keyOverride: String? = null) {
|
||||
val keyword = keyOverride?.trim() ?: queryFlow.value.trim()
|
||||
if (keyword.isBlank()) return
|
||||
|
||||
updateQuery(keyword, showSuggestions = false)
|
||||
|
||||
currentSearchId = System.currentTimeMillis()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
committedQuery = keyword,
|
||||
results = emptyList(),
|
||||
isManualStop = false,
|
||||
hasMore = true,
|
||||
processedSources = 0,
|
||||
totalSources = 0,
|
||||
emptyScopeAction = null,
|
||||
)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
repository.saveSearchKeyword(keyword)
|
||||
}
|
||||
searchSession.search(currentSearchId, keyword)
|
||||
}
|
||||
|
||||
private fun loadMore() {
|
||||
val state = _uiState.value
|
||||
if (state.isSearching) return
|
||||
if (state.committedQuery.isBlank()) return
|
||||
if (!state.hasMore) return
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isManualStop = false,
|
||||
showSuggestions = false,
|
||||
)
|
||||
}
|
||||
searchSession.search(currentSearchId, state.committedQuery)
|
||||
}
|
||||
|
||||
private fun stopSearch() {
|
||||
_uiState.update { it.copy(isManualStop = true) }
|
||||
searchSession.stop()
|
||||
}
|
||||
|
||||
private fun toggleScopeGroup(groupName: String) {
|
||||
if (searchScope.isSource()) {
|
||||
searchScope.update("")
|
||||
}
|
||||
val selected = searchScope.displayNames.toMutableSet()
|
||||
if (selected.contains(groupName)) {
|
||||
selected.remove(groupName)
|
||||
} else {
|
||||
selected.add(groupName)
|
||||
}
|
||||
searchScope.update(selected.toList())
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
private fun toggleScopeSource(source: BookSourcePart) {
|
||||
val selectedUrls = if (searchScope.isSource()) {
|
||||
searchScope.sourceUrls.toMutableSet()
|
||||
} else {
|
||||
mutableSetOf()
|
||||
}
|
||||
|
||||
if (selectedUrls.contains(source.bookSourceUrl)) {
|
||||
selectedUrls.remove(source.bookSourceUrl)
|
||||
} else {
|
||||
selectedUrls.add(source.bookSourceUrl)
|
||||
}
|
||||
|
||||
if (selectedUrls.isEmpty()) {
|
||||
searchScope.update("")
|
||||
} else {
|
||||
val selectedSources = _uiState.value.enabledSources.filter {
|
||||
selectedUrls.contains(it.bookSourceUrl)
|
||||
}
|
||||
searchScope.updateSources(selectedSources)
|
||||
}
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
private fun handleEmptyScopeActionConfirmed() {
|
||||
val action = _uiState.value.emptyScopeAction ?: return
|
||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||
|
||||
if (action.wasPrecisionSearch) {
|
||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
|
||||
_uiState.update { it.copy(isPrecisionSearch = false) }
|
||||
} else {
|
||||
searchScope.update("")
|
||||
syncScopeState()
|
||||
}
|
||||
|
||||
restartCommittedSearchIfNeeded()
|
||||
}
|
||||
|
||||
private fun restartCommittedSearchIfNeeded() {
|
||||
val committed = _uiState.value.committedQuery
|
||||
if (committed.isNotBlank()) {
|
||||
submitSearch(committed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncScopeState() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
scopeDisplay = searchScope.display,
|
||||
scopeDisplayNames = searchScope.displayNames,
|
||||
selectedScopeSourceUrls = searchScope.sourceUrls.toSet(),
|
||||
isAllScope = searchScope.isAll(),
|
||||
isSourceScope = searchScope.isSource(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState {
|
||||
val exactMatch = shelf.any {
|
||||
it.name == item.name && it.author == item.author && it.url == item.bookUrl
|
||||
}
|
||||
if (exactMatch) return BookShelfState.IN_SHELF
|
||||
|
||||
val sameNameAuthor = bookshelf.any { it.name == name && it.author == author && it.url != url }
|
||||
val sameNameAuthor = shelf.any {
|
||||
it.name == item.name && it.author == item.author && it.url != item.bookUrl
|
||||
}
|
||||
if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
|
||||
|
||||
return BookShelfState.NOT_IN_SHELF
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始搜索
|
||||
*/
|
||||
fun search(key: String) {
|
||||
execute {
|
||||
if ((searchKey == key) || key.isNotEmpty()) {
|
||||
searchModel.cancelSearch()
|
||||
searchID = System.currentTimeMillis()
|
||||
searchBookLiveData.postValue(emptyList())
|
||||
searchKey = key
|
||||
hasMore = true
|
||||
}
|
||||
if (searchKey.isEmpty()) {
|
||||
return@execute
|
||||
}
|
||||
searchModel.search(searchID, searchKey)
|
||||
}
|
||||
private fun emitEffect(effect: SearchEffect) {
|
||||
_effects.tryEmit(effect)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止搜索
|
||||
*/
|
||||
fun stop() {
|
||||
searchModel.cancelSearch()
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
searchModel.pause()
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
searchModel.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存搜索关键字
|
||||
*/
|
||||
fun saveSearchKey(key: String) {
|
||||
execute {
|
||||
appDb.searchKeywordDao.get(key)?.let {
|
||||
it.usage += 1
|
||||
it.lastUseTime = System.currentTimeMillis()
|
||||
appDb.searchKeywordDao.update(it)
|
||||
} ?: appDb.searchKeywordDao.insert(SearchKeyword(key, 1))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清楚搜索关键字
|
||||
*/
|
||||
fun clearHistory() {
|
||||
execute {
|
||||
appDb.searchKeywordDao.deleteAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteHistory(searchKeyword: SearchKeyword) {
|
||||
execute {
|
||||
appDb.searchKeywordDao.delete(searchKeyword)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
searchModel.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ 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.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -42,8 +40,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -62,10 +58,9 @@ import io.legado.app.data.entities.SearchContentHistory
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SearchBarSection
|
||||
import io.legado.app.ui.widget.components.button.MediumOutlinedButton
|
||||
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton
|
||||
@@ -225,7 +220,7 @@ fun SearchContentScreen(
|
||||
) { state ->
|
||||
when (state) {
|
||||
is SearchContentState.Error -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = state.throwable.localizedMessage ?: "发生未知错误",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -244,7 +239,7 @@ fun SearchContentScreen(
|
||||
)
|
||||
}
|
||||
SearchContentState.EmptyResult -> {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "没有找到相关内容!",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -319,7 +314,7 @@ fun SearchHistoryList(
|
||||
}
|
||||
|
||||
if (history.isEmpty()) {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "暂无搜索历史",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
|
||||
@@ -99,7 +99,7 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.CollapsibleHeader
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
|
||||
import io.legado.app.ui.widget.components.bookmark.BookmarkItem
|
||||
@@ -864,7 +864,7 @@ fun BookmarkListContent(
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "暂无书签"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
@@ -204,17 +205,17 @@ fun TxtRuleScreen(
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
data = showDeleteRuleDialog, // ä¼ å…¥ nullable çš?rule 对象
|
||||
data = showDeleteRuleDialog,
|
||||
onDismissRequest = { showDeleteRuleDialog = null },
|
||||
title = stringResource(R.string.delete),
|
||||
text = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
onConfirm = { rule ->
|
||||
viewModel.delete(rule)
|
||||
showDeleteRuleDialog = null
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = { showDeleteRuleDialog = null },
|
||||
content = { Text(text = stringResource(R.string.del_msg)) }
|
||||
onDismiss = { showDeleteRuleDialog = null }
|
||||
)
|
||||
|
||||
RuleEditSheet(
|
||||
@@ -228,7 +229,7 @@ fun TxtRuleScreen(
|
||||
editingRule = null
|
||||
},
|
||||
onSave = { updatedRule ->
|
||||
//TODO:我很想把他改为自增主键,但为了兼容性日���
|
||||
//TODO:我很想把他改为自增主键,但为了兼容性日后再�?
|
||||
if (editingRule == null) {
|
||||
viewModel.insert(updatedRule)
|
||||
} else {
|
||||
@@ -346,7 +347,7 @@ fun TxtRuleScreen(
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { showDeleteRuleDialog = item.rule },
|
||||
imageVector = Icons.Default.Delete
|
||||
imageVector = AppIcons.Delete
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,6 +18,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -36,116 +38,102 @@ fun CheckSourceBottomSheet(
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismiss
|
||||
onDismissRequest = onDismiss,
|
||||
title = stringResource(R.string.check_source_config)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = stringResource(R.string.check_source_config),
|
||||
style = LegadoTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(vertical = 16.dp)
|
||||
)
|
||||
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.check_source_timeout),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
value = viewModel.checkSourceTimeout.toFloat(),
|
||||
defaultValue = 180f,
|
||||
onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
|
||||
valueRange = 0f..300f,
|
||||
)
|
||||
GlassCard {
|
||||
SliderSettingItem(
|
||||
title = stringResource(R.string.check_source_timeout),
|
||||
value = viewModel.checkSourceTimeout.toFloat(),
|
||||
defaultValue = 180f,
|
||||
onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
|
||||
valueRange = 0f..300f,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.padding(8.dp))
|
||||
|
||||
CheckboxGroupContainer(columns = 2) {
|
||||
item {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.search),
|
||||
checked = viewModel.checkSearch,
|
||||
onCheckedChange = {
|
||||
viewModel.checkSearch = it
|
||||
if (!it && !viewModel.checkDiscovery) {
|
||||
viewModel.checkDiscovery = true
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.search),
|
||||
checked = viewModel.checkSearch,
|
||||
onCheckedChange = {
|
||||
viewModel.checkSearch = it
|
||||
if (!it && !viewModel.checkDiscovery) {
|
||||
viewModel.checkDiscovery = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
item {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.discovery),
|
||||
checked = viewModel.checkDiscovery,
|
||||
onCheckedChange = {
|
||||
viewModel.checkDiscovery = it
|
||||
if (!it && !viewModel.checkSearch) {
|
||||
viewModel.checkSearch = true
|
||||
}
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.discovery),
|
||||
checked = viewModel.checkDiscovery,
|
||||
onCheckedChange = {
|
||||
viewModel.checkDiscovery = it
|
||||
if (!it && !viewModel.checkSearch) {
|
||||
viewModel.checkSearch = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.source_tab_info),
|
||||
checked = viewModel.checkInfo,
|
||||
onCheckedChange = {
|
||||
viewModel.checkInfo = it
|
||||
if (!it) {
|
||||
viewModel.checkCategory = false
|
||||
viewModel.checkContent = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.chapter_list),
|
||||
checked = viewModel.checkCategory,
|
||||
enabled = viewModel.checkInfo,
|
||||
onCheckedChange = {
|
||||
viewModel.checkCategory = it
|
||||
if (!it) viewModel.checkContent = false
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.source_tab_content),
|
||||
checked = viewModel.checkContent,
|
||||
enabled = viewModel.checkCategory,
|
||||
onCheckedChange = { viewModel.checkContent = it }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
CheckboxGroupContainer(columns = 3) {
|
||||
item {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.source_tab_info),
|
||||
checked = viewModel.checkInfo,
|
||||
onCheckedChange = {
|
||||
viewModel.checkInfo = it
|
||||
if (!it) {
|
||||
viewModel.checkCategory = false
|
||||
viewModel.checkContent = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.chapter_list),
|
||||
checked = viewModel.checkCategory,
|
||||
enabled = viewModel.checkInfo,
|
||||
onCheckedChange = {
|
||||
viewModel.checkCategory = it
|
||||
if (!it) viewModel.checkContent = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.source_tab_content),
|
||||
checked = viewModel.checkContent,
|
||||
enabled = viewModel.checkCategory,
|
||||
onCheckedChange = { viewModel.checkContent = it }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
AppText(stringResource(R.string.cancel))
|
||||
}
|
||||
Button(onClick = {
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = {
|
||||
if (viewModel.saveCheckSourceConfig()) {
|
||||
onDismiss()
|
||||
} else {
|
||||
Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}) {
|
||||
AppText(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
-60
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.ui.config.otherConfig
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -10,6 +11,7 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Checklist
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
@@ -33,8 +35,10 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.DirectLinkUpload
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
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.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
@@ -65,57 +69,68 @@ fun DirectLinkUploadBottomSheet(
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
title = stringResource(R.string.direct_link_upload_config),
|
||||
endAction = {
|
||||
startAction = {
|
||||
MediumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert
|
||||
onClick = {
|
||||
viewModel.testRule { result -> showTestResult = result }
|
||||
},
|
||||
imageVector = Icons.Default.Checklist
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
RoundDropdownMenuItem(
|
||||
text = "导入默认",
|
||||
leadingIcon = { Icon(Icons.Default.Download, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
|
||||
viewModel.upView(rule)
|
||||
}
|
||||
}
|
||||
},
|
||||
endAction = {
|
||||
Box {
|
||||
MediumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.copy_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
val rule = DirectLinkUpload.Rule(
|
||||
viewModel.uploadUrl,
|
||||
viewModel.downloadUrlRule,
|
||||
viewModel.summary,
|
||||
viewModel.compress
|
||||
)
|
||||
context.sendToClip(GSON.toJson(rule))
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.paste_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
runCatching {
|
||||
context.getClipText()?.let {
|
||||
val rule =
|
||||
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
|
||||
.getOrThrow()
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
RoundDropdownMenuItem(
|
||||
text = "导入默认",
|
||||
leadingIcon = { Icon(Icons.Default.Download, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
|
||||
viewModel.upView(rule)
|
||||
}
|
||||
}.onFailure {
|
||||
context.toastOnUi("剪贴板为空或格式不对")
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.copy_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
val rule = DirectLinkUpload.Rule(
|
||||
viewModel.uploadUrl,
|
||||
viewModel.downloadUrlRule,
|
||||
viewModel.summary,
|
||||
viewModel.compress
|
||||
)
|
||||
context.sendToClip(GSON.toJson(rule))
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.paste_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
runCatching {
|
||||
context.getClipText()?.let {
|
||||
val rule =
|
||||
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
|
||||
.getOrThrow()
|
||||
viewModel.upView(rule)
|
||||
}
|
||||
}.onFailure {
|
||||
context.toastOnUi("剪贴板为空或格式不对")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
@@ -128,6 +143,7 @@ fun DirectLinkUploadBottomSheet(
|
||||
AppTextField(
|
||||
value = viewModel.uploadUrl,
|
||||
onValueChange = { viewModel.uploadUrl = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = stringResource(R.string.upload_url),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -137,6 +153,7 @@ fun DirectLinkUploadBottomSheet(
|
||||
AppTextField(
|
||||
value = viewModel.downloadUrlRule,
|
||||
onValueChange = { viewModel.downloadUrlRule = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = stringResource(R.string.download_url_rule),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -146,6 +163,7 @@ fun DirectLinkUploadBottomSheet(
|
||||
AppTextField(
|
||||
value = viewModel.summary,
|
||||
onValueChange = { viewModel.summary = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
label = stringResource(R.string.summary),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
@@ -154,33 +172,26 @@ fun DirectLinkUploadBottomSheet(
|
||||
|
||||
CheckboxItem(
|
||||
title = stringResource(R.string.is_compress),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
color = LegadoTheme.colorScheme.onSheetContent,
|
||||
checked = viewModel.compress,
|
||||
onCheckedChange = { viewModel.compress = it }
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(24.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
TextButton(onClick = {
|
||||
viewModel.testRule { result -> showTestResult = result }
|
||||
}) {
|
||||
AppText("测试")
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(onClick = onDismiss) {
|
||||
AppText(stringResource(R.string.cancel))
|
||||
}
|
||||
Button(onClick = {
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismiss = onDismiss,
|
||||
onConfirm = {
|
||||
if (viewModel.saveDirectLinkRule()) {
|
||||
onDismiss()
|
||||
} else {
|
||||
context.toastOnUi("请填写完整信息")
|
||||
}
|
||||
}) {
|
||||
AppText(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
confirmText = stringResource(R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
|
||||
@@ -35,24 +37,22 @@ fun PageKeySheet(
|
||||
var prevKeys by remember { mutableStateOf(ReadConfig.prevKeys) }
|
||||
var nextKeys by remember { mutableStateOf(ReadConfig.nextKeys) }
|
||||
|
||||
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) {
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.custom_page_key)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.Companion
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = stringResource(R.string.custom_page_key),
|
||||
style = LegadoTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Companion.Bold
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
AppTextField(
|
||||
value = prevKeys,
|
||||
onValueChange = { prevKeys = it },
|
||||
label = { AppText(stringResource(R.string.prev_page_key)) },
|
||||
modifier = Modifier.Companion
|
||||
label = stringResource(R.string.prev_page_key),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) {
|
||||
@@ -71,10 +71,10 @@ fun PageKeySheet(
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
AppTextField(
|
||||
value = nextKeys,
|
||||
onValueChange = { nextKeys = it },
|
||||
label = { AppText(stringResource(R.string.next_page_key)) },
|
||||
label = stringResource(R.string.next_page_key),
|
||||
modifier = Modifier.Companion
|
||||
.fillMaxWidth()
|
||||
.onPreviewKeyEvent { event ->
|
||||
@@ -96,35 +96,23 @@ fun PageKeySheet(
|
||||
|
||||
AppText(
|
||||
text = stringResource(R.string.page_key_set_help),
|
||||
style = LegadoTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
style = LegadoTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.Companion.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
prevKeys = ""
|
||||
nextKeys = ""
|
||||
},
|
||||
modifier = Modifier.Companion.weight(1f)
|
||||
) {
|
||||
AppText(stringResource(R.string.reset))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
ReadConfig.prevKeys = prevKeys
|
||||
ReadConfig.nextKeys = nextKeys
|
||||
onDismissRequest()
|
||||
},
|
||||
modifier = Modifier.Companion.weight(1f)
|
||||
) {
|
||||
AppText(stringResource(R.string.ok))
|
||||
}
|
||||
}
|
||||
ConfirmDismissButtonsRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onDismiss = {
|
||||
prevKeys = ""
|
||||
nextKeys = ""
|
||||
},
|
||||
onConfirm = {
|
||||
ReadConfig.prevKeys = prevKeys
|
||||
ReadConfig.nextKeys = nextKeys
|
||||
onDismissRequest()
|
||||
},
|
||||
dismissText = stringResource(R.string.reset),
|
||||
confirmText = stringResource(R.string.ok)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-23
@@ -31,7 +31,9 @@ import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -68,35 +70,31 @@ fun BackgroundImageManageSheet(
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.background_image),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
AppText(
|
||||
text = stringResource(R.string.background_image),
|
||||
style = LegadoTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
|
||||
if (currentPath.isNullOrBlank()) {
|
||||
Surface(
|
||||
onClick = { showFilePicker = true },
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
color = LegadoTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(16f / 9f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Icon(
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add",
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
tint = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -119,24 +117,14 @@ fun BackgroundImageManageSheet(
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
SmallTonalIconButton(
|
||||
onClick = { viewModel.removeBackground(isDarkTheme) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(32.dp)
|
||||
) {
|
||||
Surface(
|
||||
shape = CircleShape,
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove",
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
.size(32.dp),
|
||||
imageVector = Icons.Default.Close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
@@ -320,7 +321,7 @@ fun DictRuleScreen(
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { showDeleteRuleDialog = item.rule },
|
||||
imageVector = Icons.Default.Delete
|
||||
imageVector = AppIcons.Delete
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -376,6 +376,7 @@ fun BookGroupItemList(
|
||||
fun BookItem(
|
||||
book: BookShelfItem,
|
||||
layoutMode: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
gridStyle: Int = 0,
|
||||
isCompact: Boolean = false,
|
||||
isUpdating: Boolean = false,
|
||||
@@ -403,6 +404,7 @@ fun BookItem(
|
||||
isGrid = layoutMode != 0,
|
||||
gridStyle = gridStyle,
|
||||
isCompact = isCompact,
|
||||
modifier = modifier,
|
||||
cover = { modifier ->
|
||||
BookshelfCover(
|
||||
name = book.name,
|
||||
|
||||
@@ -74,6 +74,8 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
@@ -197,20 +199,18 @@ fun BookshelfScreen(
|
||||
}
|
||||
|
||||
val bookGroupStyle = BookshelfConfig.bookGroupStyle
|
||||
// 控制是å�¦å¤„于“文件夹列表â€�æ ¹è§†å›¾ï¼Œè¿˜æ˜¯â€œæ–‡ä»¶å¤¹å†…éƒ¨â€�书ç±�视å›?
|
||||
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
|
||||
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
|
||||
val baseTitle = when {
|
||||
bookGroupStyle == 1 -> {
|
||||
val baseTitle = when (bookGroupStyle) {
|
||||
1 -> {
|
||||
uiState.groups.getOrNull(pagerState.currentPage)?.groupName
|
||||
?: stringResource(R.string.bookshelf)
|
||||
}
|
||||
|
||||
bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
|
||||
2 if uiState.groups.isNotEmpty() -> {
|
||||
if (isInFolderRoot) stringResource(R.string.bookshelf)
|
||||
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName
|
||||
?: stringResource(R.string.bookshelf)
|
||||
}
|
||||
|
||||
else -> stringResource(R.string.bookshelf)
|
||||
}
|
||||
val title = if (uiState.upBooksCount > 0) {
|
||||
@@ -329,7 +329,7 @@ fun BookshelfScreen(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.adaptiveHorizontalPadding(),
|
||||
.adaptiveHorizontalPaddingTab(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val tabTitles = remember(uiState.groups) {
|
||||
@@ -600,6 +600,19 @@ fun BookshelfPage(
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit
|
||||
) {
|
||||
if (books.isEmpty()) {
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding()
|
||||
),
|
||||
messageResId = R.string.bookshelf_empty
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
||||
val isGridMode = bookshelfLayoutMode != 0
|
||||
val totalHorizontalPadding =
|
||||
@@ -623,6 +636,7 @@ fun BookshelfPage(
|
||||
items(books, key = { it.bookUrl }) { book ->
|
||||
BookItem(
|
||||
book = book,
|
||||
modifier = Modifier.animateItem(),
|
||||
layoutMode = bookshelfLayoutMode,
|
||||
gridStyle = BookshelfConfig.bookshelfGridLayout,
|
||||
isCompact = BookshelfConfig.bookshelfLayoutCompact,
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.ui.book.group.GroupEditContent
|
||||
import io.legado.app.ui.book.group.GroupDeleteAction
|
||||
import io.legado.app.ui.book.group.GroupResetCoverAction
|
||||
import io.legado.app.ui.book.group.GroupViewModel
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
@@ -48,6 +50,7 @@ fun GroupManageSheet(
|
||||
val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
|
||||
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
|
||||
var isEditing by remember { mutableStateOf(false) }
|
||||
var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) }
|
||||
|
||||
var listData by remember { mutableStateOf(groups) }
|
||||
val listState = rememberLazyListState()
|
||||
@@ -76,15 +79,36 @@ fun GroupManageSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = if (!isEditing) stringResource(R.string.group_manage) else stringResource(R.string.group_edit),
|
||||
startAction = editingGroup?.takeIf {
|
||||
isEditing && (it.groupId > 0 || it.groupId == Long.MIN_VALUE)
|
||||
}?.let { group ->
|
||||
{
|
||||
GroupDeleteAction(
|
||||
group = group,
|
||||
onDismissRequest = {
|
||||
editingGroup = null
|
||||
isEditing = false
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
},
|
||||
endAction = {
|
||||
if (!isEditing) {
|
||||
SmallIconButton(
|
||||
onClick = {
|
||||
editingGroup = null
|
||||
coverPath = null
|
||||
isEditing = true
|
||||
},
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
} else {
|
||||
GroupResetCoverAction(
|
||||
group = editingGroup,
|
||||
onCoverPathChange = { coverPath = it },
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
@@ -98,7 +122,12 @@ fun GroupManageSheet(
|
||||
if (editing) {
|
||||
GroupEditContent(
|
||||
group = editingGroup,
|
||||
onDismissRequest = { isEditing = false },
|
||||
onDismissRequest = {
|
||||
editingGroup = null
|
||||
isEditing = false
|
||||
},
|
||||
coverPath = coverPath,
|
||||
onCoverPathChange = { coverPath = it },
|
||||
viewModel = viewModel
|
||||
)
|
||||
} else {
|
||||
@@ -115,12 +144,13 @@ fun GroupManageSheet(
|
||||
title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
|
||||
subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
|
||||
isEnabled = group.show,
|
||||
containerColor = LegadoTheme.colorScheme.surface,
|
||||
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||
onEnabledChange = { isChecked ->
|
||||
viewModel.upGroup(group.copy(show = isChecked))
|
||||
},
|
||||
onClickEdit = {
|
||||
editingGroup = group
|
||||
coverPath = group.cover
|
||||
isEditing = true
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,6 +67,7 @@ import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
||||
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
|
||||
import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
||||
@@ -159,6 +160,19 @@ fun ExploreScreen(
|
||||
}
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
if (uiState.items.isEmpty()) {
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = paddingValues.calculateBottomPadding()
|
||||
),
|
||||
messageResId = R.string.explore_empty
|
||||
)
|
||||
return@Box
|
||||
}
|
||||
|
||||
FastScrollLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
@@ -72,6 +72,7 @@ import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AlertButton
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.button.ToggleChip
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
||||
@@ -368,21 +369,20 @@ fun GroupSelector(
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onManageClick) {
|
||||
Icon(Icons.Default.Settings, "Manage")
|
||||
}
|
||||
MediumIconButton(
|
||||
onClick = onManageClick,
|
||||
imageVector = Icons.Default.Settings
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ManageGroupDialog(
|
||||
show: Boolean, // 💡 新增 show 参数,外层直接传值,不要用 if 包裹!
|
||||
show: Boolean,
|
||||
groups: List<String>,
|
||||
onDismiss: () -> Unit,
|
||||
onDelete: (List<String>) -> Unit
|
||||
) {
|
||||
// 💡 使用 Set 来管理多选状态,并且以 show 作为 key。
|
||||
// 这样每次重新打开弹窗时,选中状态会自动清空,防止上次的残留
|
||||
var selectedGroups by remember(show) { mutableStateOf(emptySet<String>()) }
|
||||
|
||||
AppAlertDialog(
|
||||
@@ -395,7 +395,6 @@ fun ManageGroupDialog(
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
// 去掉了 spacedBy(8.dp),因为 CheckboxItem 内部已经有了合适的 padding
|
||||
) {
|
||||
groups.forEach { group ->
|
||||
val isSelected = selectedGroups.contains(group)
|
||||
@@ -418,7 +417,6 @@ fun ManageGroupDialog(
|
||||
confirmText = "删除选中",
|
||||
onConfirm = {
|
||||
onDelete(selectedGroups.toList())
|
||||
// 注意:外层需要在 onDelete 回调中将 show 置为 false
|
||||
},
|
||||
dismissText = "关闭",
|
||||
onDismiss = onDismiss
|
||||
|
||||
@@ -34,7 +34,7 @@ import io.legado.app.R
|
||||
import io.legado.app.data.entities.RssStar
|
||||
import io.legado.app.ui.rss.read.ReadRssActivity
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.SourceIcon
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
@@ -186,7 +186,7 @@ fun RssFavoritesScreen(
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = "还没有收藏订阅!",
|
||||
isLoading = state.isLoading
|
||||
)
|
||||
@@ -203,7 +203,7 @@ fun RssFavoritesScreen(
|
||||
SelectionItemCard(
|
||||
title = rssStar.title,
|
||||
subtitle = if (rssStar.group.isNotBlank()) {
|
||||
"${rssStar.group} �${rssStar.pubDate ?: ""}"
|
||||
"${rssStar.group} �?${rssStar.pubDate ?: ""}"
|
||||
} else {
|
||||
rssStar.pubDate
|
||||
},
|
||||
|
||||
@@ -16,15 +16,12 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -49,7 +46,7 @@ import io.legado.app.ui.association.ImportReplaceRuleDialog
|
||||
import io.legado.app.ui.association.ImportRssSourceDialog
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.EmptyMessage
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
|
||||
@@ -123,7 +120,7 @@ fun RuleSubScreen(
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = stringResource(R.string.rule_sub_empty_msg),
|
||||
isLoading = state.isLoading
|
||||
)
|
||||
|
||||
@@ -14,6 +14,13 @@ fun Modifier.adaptiveHorizontalPadding(): Modifier {
|
||||
return this.padding(horizontal = horizontal)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.adaptiveHorizontalPaddingTab(): Modifier {
|
||||
val start = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 0.dp
|
||||
val end = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
|
||||
return this.padding(start = start, end = end)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.adaptiveHorizontalPadding(
|
||||
vertical: Dp,
|
||||
|
||||
@@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||
|
||||
@@ -194,8 +196,10 @@ fun AppTheme(
|
||||
onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
|
||||
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
|
||||
|
||||
cardContainer = miuixColorScheme.disabledPrimary,
|
||||
onCardContainer = miuixColorScheme.primary
|
||||
cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f)
|
||||
.compositeOver(miuixColorScheme.surfaceContainer),
|
||||
onCardContainer = miuixColorScheme.primary,
|
||||
onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -229,32 +233,3 @@ fun AppTheme(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Typography.toLegadoTypography(): LegadoTypography {
|
||||
return LegadoTypography(
|
||||
headlineLarge = headlineLarge,
|
||||
headlineLargeEmphasized = headlineLargeEmphasized,
|
||||
headlineMedium = headlineMedium,
|
||||
headlineMediumEmphasized = headlineMediumEmphasized,
|
||||
headlineSmall = headlineSmall,
|
||||
headlineSmallEmphasized = headlineSmallEmphasized,
|
||||
titleLarge = titleLarge,
|
||||
titleLargeEmphasized = titleLargeEmphasized,
|
||||
titleMedium = titleMedium,
|
||||
titleMediumEmphasized = titleMediumEmphasized,
|
||||
titleSmall = titleSmall,
|
||||
titleSmallEmphasized = titleSmallEmphasized,
|
||||
bodyLarge = bodyLarge,
|
||||
bodyLargeEmphasized = bodyLargeEmphasized,
|
||||
bodyMedium = bodyMedium,
|
||||
bodyMediumEmphasized = bodyMediumEmphasized,
|
||||
bodySmall = bodySmall,
|
||||
bodySmallEmphasized = bodySmallEmphasized,
|
||||
labelLarge = labelLarge,
|
||||
labelLargeEmphasized = labelLargeEmphasized,
|
||||
labelMedium = labelMedium,
|
||||
labelMediumEmphasized = labelMediumEmphasized,
|
||||
labelSmall = labelSmall,
|
||||
labelSmallEmphasized = labelSmallEmphasized
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,29 @@ fun Modifier.responsiveHazeEffect(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
fun Modifier.responsiveHazeEffectFixedStyle(
|
||||
state: HazeState
|
||||
): Modifier {
|
||||
val enableBlur = ThemeConfig.enableBlur
|
||||
|
||||
if (!enableBlur) return this
|
||||
|
||||
val style = HazeLegado.ultraThinPlus()
|
||||
|
||||
return this.hazeEffect(
|
||||
state = state,
|
||||
style = style
|
||||
) {
|
||||
progressive =
|
||||
HazeProgressive.verticalGradient(
|
||||
startIntensity = 1f,
|
||||
endIntensity = 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅判断 enableBlur 的简单 HazeEffect
|
||||
*/
|
||||
|
||||
@@ -14,14 +14,15 @@ import androidx.core.graphics.scale
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import coil.size.Size
|
||||
import com.materialkolor.quantize.QuantizerCelebi
|
||||
import com.materialkolor.score.Score
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 256
|
||||
private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 128
|
||||
private const val IMAGE_MAX_QUANTIZE_COLORS = 128
|
||||
private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 128
|
||||
private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 64
|
||||
private const val IMAGE_MAX_QUANTIZE_COLORS = 64
|
||||
private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt()
|
||||
|
||||
suspend fun ImageLoader.extractSeedColor(
|
||||
@@ -32,6 +33,7 @@ suspend fun ImageLoader.extractSeedColor(
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.allowHardware(false)
|
||||
.size(Size(IMAGE_COLOR_EXTRACT_SIZE_PX, IMAGE_COLOR_EXTRACT_SIZE_PX))
|
||||
.apply(configureRequest)
|
||||
.build()
|
||||
|
||||
|
||||
@@ -73,7 +73,8 @@ data class LegadoColorScheme(
|
||||
val onTertiaryFixedVariant: Color,
|
||||
|
||||
val cardContainer: Color,
|
||||
val onCardContainer: Color
|
||||
val onCardContainer: Color,
|
||||
val onSheetContent: Color
|
||||
)
|
||||
|
||||
data class LegadoTypography(
|
||||
|
||||
@@ -68,7 +68,8 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
||||
onTertiaryFixed = onTertiaryFixed,
|
||||
onTertiaryFixedVariant = onTertiaryFixedVariant,
|
||||
cardContainer = primaryContainer.copy(alpha = 0.5f),
|
||||
onCardContainer = primary
|
||||
onCardContainer = primary,
|
||||
onSheetContent = surface
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ fun ProvideThemeOverride(
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
var appliedTheme by remember { mutableStateOf<ThemeOverrideState?>(null) }
|
||||
val baseTheme = LocalLegadoThemeColors.current
|
||||
|
||||
LaunchedEffect(theme) {
|
||||
if (theme == null) {
|
||||
@@ -66,15 +67,16 @@ fun ProvideThemeOverride(
|
||||
}
|
||||
|
||||
val currentTheme = appliedTheme
|
||||
if (currentTheme == null) {
|
||||
content()
|
||||
} else {
|
||||
ProvideColorSchemeOverride(
|
||||
colorScheme = currentTheme.colorScheme,
|
||||
seedColor = currentTheme.seedColor,
|
||||
content = content
|
||||
?: ThemeOverrideState(
|
||||
seedColor = baseTheme.seedColor,
|
||||
colorScheme = baseTheme.colorScheme
|
||||
)
|
||||
}
|
||||
|
||||
ProvideColorSchemeOverride(
|
||||
colorScheme = currentTheme.colorScheme,
|
||||
seedColor = currentTheme.seedColor,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -49,3 +49,33 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
|
||||
labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
fun Typography.toLegadoTypography(): LegadoTypography {
|
||||
return LegadoTypography(
|
||||
headlineLarge = headlineLarge,
|
||||
headlineLargeEmphasized = headlineLargeEmphasized,
|
||||
headlineMedium = headlineMedium,
|
||||
headlineMediumEmphasized = headlineMediumEmphasized,
|
||||
headlineSmall = headlineSmall,
|
||||
headlineSmallEmphasized = headlineSmallEmphasized,
|
||||
titleLarge = titleLarge,
|
||||
titleLargeEmphasized = titleLargeEmphasized,
|
||||
titleMedium = titleMedium,
|
||||
titleMediumEmphasized = titleMediumEmphasized,
|
||||
titleSmall = titleSmall,
|
||||
titleSmallEmphasized = titleSmallEmphasized,
|
||||
bodyLarge = bodyLarge,
|
||||
bodyLargeEmphasized = bodyLargeEmphasized,
|
||||
bodyMedium = bodyMedium,
|
||||
bodyMediumEmphasized = bodyMediumEmphasized,
|
||||
bodySmall = bodySmall,
|
||||
bodySmallEmphasized = bodySmallEmphasized,
|
||||
labelLarge = labelLarge,
|
||||
labelLargeEmphasized = labelLargeEmphasized,
|
||||
labelMedium = labelMedium,
|
||||
labelMediumEmphasized = labelMediumEmphasized,
|
||||
labelSmall = labelSmall,
|
||||
labelSmallEmphasized = labelSmallEmphasized
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
object HazeLegado {
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun ultraThinPlus(
|
||||
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
||||
): HazeStyle = hazeLegado(
|
||||
containerColor = containerColor,
|
||||
lightAlpha = 0.0f,
|
||||
darkAlpha = 0.0f,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun ultraThin(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
// 这里建议使用 as 关键字为两个库的控件起别名,避免同名冲突
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
// 请替换为实际的 MIUIX 控件包名
|
||||
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator as MiuixLinearProgressIndicator
|
||||
|
||||
@Composable
|
||||
fun AppLinearProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
progress: Float? = null,
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixLinearProgressIndicator(
|
||||
modifier = modifier,
|
||||
progress = progress
|
||||
)
|
||||
} else {
|
||||
if (progress != null) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.material3.ExpandedFullScreenContainedSearchBar
|
||||
import androidx.compose.material3.ExpandedFullScreenSearchBar
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SearchBarValue
|
||||
import androidx.compose.material3.SearchBarValue.Collapsed
|
||||
import androidx.compose.material3.SearchBarValue.Expanded
|
||||
import androidx.compose.material3.rememberSearchBarState
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.InputField as MiuixInputField
|
||||
import top.yukonga.miuix.kmp.basic.SearchBar as MiuixSearchBar
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun AppSearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
expanded: Boolean,
|
||||
onExpandedChange: (Boolean) -> Unit,
|
||||
label: String,
|
||||
placeholder: @Composable (() -> Unit)? = null,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
if (isMiuix) {
|
||||
var internalExpanded by remember { mutableStateOf(expanded) }
|
||||
var internalQuery by remember { mutableStateOf(query) }
|
||||
|
||||
LaunchedEffect(expanded) {
|
||||
internalExpanded = expanded
|
||||
}
|
||||
|
||||
LaunchedEffect(query) {
|
||||
if (query != internalQuery) {
|
||||
internalQuery = query
|
||||
}
|
||||
}
|
||||
|
||||
MiuixSearchBar(
|
||||
modifier = modifier,
|
||||
inputField = {
|
||||
MiuixInputField(
|
||||
query = internalQuery,
|
||||
onQueryChange = { newQuery ->
|
||||
if (newQuery.isNotEmpty() || internalExpanded) {
|
||||
internalQuery = newQuery
|
||||
onQueryChange(newQuery)
|
||||
}
|
||||
},
|
||||
onSearch = {
|
||||
onSearch(it)
|
||||
onExpandedChange(false)
|
||||
},
|
||||
expanded = internalExpanded,
|
||||
onExpandedChange = { newExpanded ->
|
||||
internalExpanded = newExpanded
|
||||
onExpandedChange(newExpanded)
|
||||
},
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
)
|
||||
},
|
||||
expanded = internalExpanded,
|
||||
onExpandedChange = onExpandedChange,
|
||||
content = content,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val initialSearchBarValue = remember {
|
||||
if (expanded) SearchBarValue.Expanded else SearchBarValue.Collapsed
|
||||
}
|
||||
val searchBarState = rememberSearchBarState(initialValue = initialSearchBarValue)
|
||||
val textFieldState = rememberTextFieldState(initialText = query)
|
||||
val scope = rememberCoroutineScope()
|
||||
val latestQuery by rememberUpdatedState(query)
|
||||
val latestExpanded by rememberUpdatedState(expanded)
|
||||
val latestOnQueryChange by rememberUpdatedState(onQueryChange)
|
||||
val latestOnExpandedChange by rememberUpdatedState(onExpandedChange)
|
||||
val latestOnSearch by rememberUpdatedState(onSearch)
|
||||
|
||||
LaunchedEffect(query) {
|
||||
if (query != textFieldState.text.toString()) {
|
||||
textFieldState.setTextAndPlaceCursorAtEnd(query)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(textFieldState) {
|
||||
snapshotFlow { textFieldState.text.toString() }
|
||||
.distinctUntilChanged()
|
||||
.collect { newQuery ->
|
||||
if (newQuery != latestQuery) {
|
||||
latestOnQueryChange(newQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(expanded) {
|
||||
if (expanded && searchBarState.targetValue != Expanded) {
|
||||
searchBarState.animateToExpanded()
|
||||
} else if (!expanded && searchBarState.targetValue != Collapsed) {
|
||||
searchBarState.animateToCollapsed()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(searchBarState) {
|
||||
snapshotFlow { searchBarState.currentValue == Expanded }
|
||||
.distinctUntilChanged()
|
||||
.collect { isExpanded ->
|
||||
if (isExpanded != latestExpanded) {
|
||||
latestOnExpandedChange(isExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val inputField: @Composable () -> Unit = {
|
||||
SearchBarDefaults.InputField(
|
||||
textFieldState = textFieldState,
|
||||
searchBarState = searchBarState,
|
||||
onSearch = { keyword ->
|
||||
latestOnSearch(keyword)
|
||||
scope.launch { searchBarState.animateToCollapsed() }
|
||||
},
|
||||
placeholder = placeholder,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
)
|
||||
}
|
||||
|
||||
SearchBar(
|
||||
modifier = modifier.padding(horizontal = 16.dp),
|
||||
state = searchBarState,
|
||||
inputField = inputField
|
||||
)
|
||||
|
||||
ExpandedFullScreenSearchBar(
|
||||
state = searchBarState,
|
||||
inputField = inputField,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.basic.InputField as MiuixSearchBarInputField
|
||||
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -300,13 +301,48 @@ fun AppDenseTextField(
|
||||
scrollState: ScrollState = rememberScrollState(),
|
||||
shape: Shape = TextFieldDefaults.shape,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
miuixUseSearchBarInputField: Boolean = false,
|
||||
miuixSearchBarLabel: String = label ?: "",
|
||||
miuixOnSearch: (String) -> Unit = {},
|
||||
) {
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
if (isMiuix && miuixUseSearchBarInputField) {
|
||||
MiuixSearchBarInputField(
|
||||
query = state.text.toString(),
|
||||
onQueryChange = { newQuery ->
|
||||
val current = state.text.toString()
|
||||
if (newQuery != current) {
|
||||
state.edit {
|
||||
replace(0, length, newQuery)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSearch = miuixOnSearch,
|
||||
expanded = false,
|
||||
onExpandedChange = {},
|
||||
modifier = modifier.heightIn(min = 45.dp),
|
||||
label = miuixSearchBarLabel,
|
||||
enabled = enabled,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val denseMinHeight = if (isMiuix) 45.dp else 48.dp
|
||||
val denseBackgroundColor = if (isMiuix && backgroundColor == Color.Unspecified) {
|
||||
MiuixTheme.colorScheme.surfaceContainerHigh
|
||||
} else {
|
||||
backgroundColor
|
||||
}
|
||||
|
||||
AppTextField(
|
||||
state = state,
|
||||
modifier = modifier.heightIn(min = 48.dp),
|
||||
modifier = modifier.heightIn(min = denseMinHeight),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
backgroundColor = backgroundColor,
|
||||
backgroundColor = denseBackgroundColor,
|
||||
label = label,
|
||||
labelPosition = labelPosition,
|
||||
placeholder = placeholder,
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun EmptyMessageView(
|
||||
fun EmptyMessage(
|
||||
message: String,
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
@@ -81,7 +81,7 @@ fun EmptyMessageView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyMessageView(
|
||||
fun EmptyMessage(
|
||||
@StringRes messageResId: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
isLoading: Boolean = false,
|
||||
@@ -93,7 +93,7 @@ fun EmptyMessageView(
|
||||
onFaceClick: (() -> Unit)? = null
|
||||
) {
|
||||
val message = stringResource(id = messageResId)
|
||||
EmptyMessageView(
|
||||
EmptyMessage(
|
||||
message = message,
|
||||
modifier = modifier,
|
||||
isLoading = isLoading,
|
||||
@@ -9,14 +9,9 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
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.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -49,7 +44,6 @@ fun SearchBarSection(
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
val textFieldState = rememberTextFieldState(initialText = query)
|
||||
|
||||
LaunchedEffect(query) {
|
||||
@@ -76,30 +70,20 @@ fun SearchBarSection(
|
||||
if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
|
||||
val modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (isMiuix) modifier.padding(vertical = 4.dp)
|
||||
else modifier
|
||||
)
|
||||
|
||||
val searchTextField = @Composable {
|
||||
if (isMiuix) {
|
||||
AppDenseTextField(
|
||||
state = textFieldState,
|
||||
modifier = modifier,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
placeholder = { AppText(placeholder) },
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
backgroundColor = if (!isMiuix) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
resolvedBackgroundColor
|
||||
}
|
||||
backgroundColor = resolvedBackgroundColor,
|
||||
miuixUseSearchBarInputField = true,
|
||||
miuixSearchBarLabel = placeholder
|
||||
)
|
||||
}
|
||||
|
||||
if (isMiuix) {
|
||||
searchTextField()
|
||||
} else {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
@@ -108,7 +92,15 @@ fun SearchBarSection(
|
||||
shape = RoundedCornerShape(32.dp),
|
||||
color = resolvedBackgroundColor
|
||||
) {
|
||||
searchTextField()
|
||||
AppDenseTextField(
|
||||
state = textFieldState,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
placeholder = { AppText(placeholder) },
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
backgroundColor = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ fun <T> AppAlertDialog(
|
||||
data: T?,
|
||||
onDismissRequest: () -> Unit,
|
||||
title: String? = null,
|
||||
text: String? = null,
|
||||
confirmText: String = "确定",
|
||||
onConfirm: ((T) -> Unit)? = null,
|
||||
dismissText: String = "取消",
|
||||
@@ -173,6 +174,7 @@ fun <T> AppAlertDialog(
|
||||
show = data != null,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = title,
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
confirmText = confirmText,
|
||||
onConfirm = onConfirm?.let { { it(currentData) } },
|
||||
|
||||
@@ -73,7 +73,8 @@ fun SmallIconButton(
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(smallIconSize),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package io.legado.app.ui.widget.components.button
|
||||
|
||||
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
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -15,9 +21,11 @@ import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
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.Text as MiuixText
|
||||
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
|
||||
|
||||
@Composable
|
||||
fun SmallTextButton(
|
||||
@@ -27,14 +35,31 @@ fun SmallTextButton(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixButton(
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
colors = MiuixButtonDefaults.buttonColors()
|
||||
) {
|
||||
MiuixText(
|
||||
text = text
|
||||
showIndication = true,
|
||||
colors = CardDefaults.defaultColors(
|
||||
color = LegadoTheme.colorScheme.surfaceVariant,
|
||||
contentColor = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
AppText(
|
||||
text = text,
|
||||
style = LegadoTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
|
||||
@@ -46,14 +46,15 @@ fun ToggleChip(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
AnimatedVisibility(visible = selected) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MiuixIcon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = checkedContentDescription,
|
||||
modifier = Modifier.size(16.dp)
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
}
|
||||
@@ -61,9 +62,10 @@ fun ToggleChip(
|
||||
|
||||
MiuixText(
|
||||
text = label,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
style = LegadoTheme.typography.labelMediumEmphasized,
|
||||
maxLines = 1,
|
||||
softWrap = false
|
||||
softWrap = false,
|
||||
color = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.card
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -39,15 +40,24 @@ private fun BaseCard(
|
||||
color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)),
|
||||
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
|
||||
)
|
||||
MiuixCard(
|
||||
modifier = modifier,
|
||||
cornerRadius = cornerRadius,
|
||||
pressFeedbackType = pressFeedbackType,
|
||||
showIndication = true,
|
||||
onClick = onClick,
|
||||
content = content,
|
||||
colors = colors
|
||||
)
|
||||
if (onClick != null) {
|
||||
MiuixCard(
|
||||
modifier = modifier,
|
||||
cornerRadius = cornerRadius,
|
||||
pressFeedbackType = pressFeedbackType,
|
||||
showIndication = true,
|
||||
onClick = onClick,
|
||||
content = content,
|
||||
colors = colors
|
||||
)
|
||||
} else {
|
||||
MiuixCard(
|
||||
modifier = modifier,
|
||||
cornerRadius = cornerRadius,
|
||||
content = content,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val colors = CardDefaults.cardColors(
|
||||
containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy(
|
||||
@@ -60,7 +70,7 @@ private fun BaseCard(
|
||||
if (onClick != null) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
@@ -70,7 +80,7 @@ private fun BaseCard(
|
||||
} else {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
colors = colors,
|
||||
elevation = elevation,
|
||||
border = border,
|
||||
|
||||
@@ -44,6 +44,8 @@ import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.AdaptiveSwitch
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.checkBox.AppCheckbox
|
||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import sh.calvin.reorderable.ReorderableItem
|
||||
@@ -70,8 +72,6 @@ fun SelectionItemCard(
|
||||
containerColor: Color? = null,
|
||||
selectedContainerColor: Color? = null
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val composeEngine = ThemeResolver.isMiuixEngine(composeEngine)
|
||||
val animatedContainerColor by animateColorAsState(
|
||||
targetValue = if (isSelected)
|
||||
@@ -92,48 +92,104 @@ fun SelectionItemCard(
|
||||
containerColor = animatedContainerColor,
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
SelectionItemCardContent(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
supportingContent = supportingContent,
|
||||
isEnabled = isEnabled,
|
||||
isSelected = isSelected,
|
||||
inSelectionMode = inSelectionMode,
|
||||
leadingContent = leadingContent,
|
||||
onEnabledChange = onEnabledChange,
|
||||
onClickEdit = onClickEdit,
|
||||
trailingAction = trailingAction,
|
||||
dropdownContent = dropdownContent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectionItemCardContent(
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
supportingContent: @Composable (() -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
isSelected: Boolean = false,
|
||||
inSelectionMode: Boolean = false,
|
||||
leadingContent: @Composable (() -> Unit)? = null,
|
||||
onEnabledChange: ((Boolean) -> Unit)? = null,
|
||||
onClickEdit: (() -> Unit)? = null,
|
||||
trailingAction: @Composable (RowScope.() -> Unit)? = null,
|
||||
dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = inSelectionMode || leadingContent != null
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = inSelectionMode || leadingContent != null
|
||||
Box(
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = inSelectionMode,
|
||||
label = "LeadingContent"
|
||||
) { selectionMode ->
|
||||
if (selectionMode) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null
|
||||
)
|
||||
} else {
|
||||
leadingContent?.invoke()
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = inSelectionMode,
|
||||
label = "LeadingContent"
|
||||
) { selectionMode ->
|
||||
if (selectionMode) {
|
||||
AppCheckbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null
|
||||
)
|
||||
} else {
|
||||
leadingContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (composeEngine) {
|
||||
BasicComponent(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
BasicComponent(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
when {
|
||||
supportingContent != null -> supportingContent()
|
||||
!subtitle.isNullOrBlank() -> {
|
||||
AppText(
|
||||
text = subtitle,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ListItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
headlineContent = {
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
when {
|
||||
supportingContent != null -> supportingContent()
|
||||
!subtitle.isNullOrBlank() -> {
|
||||
},
|
||||
supportingContent = when {
|
||||
supportingContent != null -> supportingContent
|
||||
!subtitle.isNullOrBlank() -> {
|
||||
{
|
||||
AppText(
|
||||
text = subtitle,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
@@ -142,75 +198,50 @@ fun SelectionItemCard(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ListItem(
|
||||
modifier = Modifier.weight(1f),
|
||||
headlineContent = {
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = when {
|
||||
supportingContent != null -> supportingContent
|
||||
!subtitle.isNullOrBlank() -> {
|
||||
{
|
||||
AppText(
|
||||
text = subtitle,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
||||
else -> null
|
||||
},
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
onEnabledChange?.let {
|
||||
AdaptiveSwitch(
|
||||
modifier = Modifier.scale(0.8f),
|
||||
checked = isEnabled,
|
||||
onCheckedChange = it
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
onEnabledChange?.let {
|
||||
AdaptiveSwitch(
|
||||
modifier = Modifier.scale(0.8f),
|
||||
checked = isEnabled,
|
||||
onCheckedChange = it
|
||||
)
|
||||
}
|
||||
if (onClickEdit != null) {
|
||||
SmallIconButton(
|
||||
onClick = onClickEdit,
|
||||
imageVector = AppIcons.Edit,
|
||||
contentDescription = "Edit"
|
||||
)
|
||||
}
|
||||
|
||||
if (onClickEdit != null) {
|
||||
if (trailingAction != null) {
|
||||
trailingAction()
|
||||
}
|
||||
|
||||
if (dropdownContent != null) {
|
||||
Box {
|
||||
SmallIconButton(
|
||||
onClick = onClickEdit,
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Edit"
|
||||
onClick = { showMenu = true },
|
||||
imageVector = AppIcons.MoreVert,
|
||||
contentDescription = "More"
|
||||
)
|
||||
}
|
||||
|
||||
if (trailingAction != null) {
|
||||
trailingAction()
|
||||
}
|
||||
|
||||
if (dropdownContent != null) {
|
||||
Box {
|
||||
SmallIconButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More"
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
dropdownContent { showMenu = false }
|
||||
}
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
dropdownContent { showMenu = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,7 @@ fun TextCard(
|
||||
|
||||
NormalCard(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(cornerRadius),
|
||||
cornerRadius = 4.dp,
|
||||
cornerRadius = cornerRadius,
|
||||
containerColor = finalBackgroundColor,
|
||||
contentColor = finalContentColor,
|
||||
onClick = onClick
|
||||
|
||||
@@ -14,44 +14,45 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
|
||||
@Composable
|
||||
fun CheckboxItem(
|
||||
title: String,
|
||||
color: Color = Color.Transparent,
|
||||
color: Color = LegadoTheme.colorScheme.onSheetContent,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
val alpha = if (enabled) 1f else 0.5f
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(
|
||||
color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer
|
||||
else color,
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
GlassCard(
|
||||
cornerRadius = 12.dp,
|
||||
containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
|
||||
) {
|
||||
AppCheckbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.alpha(alpha)
|
||||
)
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.bodyMediumEmphasized,
|
||||
maxLines = 1,
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.alpha(alpha)
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppCheckbox(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
modifier = Modifier.alpha(alpha)
|
||||
)
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.bodyMediumEmphasized,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp)
|
||||
.alpha(alpha)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,9 +39,10 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.withSave
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import org.koin.compose.koinInject
|
||||
import io.legado.app.model.BookCover as BookCoverModel
|
||||
|
||||
@@ -81,86 +82,93 @@ fun BookCover(
|
||||
isFinalPathLoaded = false
|
||||
}
|
||||
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.aspectRatio(5f / 7f)
|
||||
.then(
|
||||
if (CoverConfig.coverShowShadow) {
|
||||
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||
} else Modifier
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.then(
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val iconSizeDp = with(density) {
|
||||
(minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
|
||||
}
|
||||
NormalCard(
|
||||
cornerRadius = 4.dp
|
||||
){
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.aspectRatio(5f / 7f)
|
||||
.then(
|
||||
if (CoverConfig.coverShowShadow) {
|
||||
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||
} else Modifier
|
||||
)
|
||||
.then(
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
|
||||
} else Modifier
|
||||
)
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val iconSizeDp = with(density) {
|
||||
(minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
|
||||
}
|
||||
|
||||
if (hasCustomDefault && !isFinalPathLoaded) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(randomPath)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
if (hasCustomDefault && !isFinalPathLoaded) {
|
||||
AsyncImage(
|
||||
model = buildCoverImageRequest(
|
||||
context = context,
|
||||
data = randomPath,
|
||||
sourceOrigin = null,
|
||||
loadOnlyWifi = false,
|
||||
crossfade = true,
|
||||
),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
if (finalPath != null) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(finalPath)
|
||||
.crossfade(true)
|
||||
.setParameter("sourceOrigin", sourceOrigin)
|
||||
.setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onSuccess = {
|
||||
isOnlineCoverLoaded = true
|
||||
isFinalPathLoaded = true
|
||||
onLoadFinish?.invoke()
|
||||
},
|
||||
onError = {
|
||||
isOnlineCoverLoaded = false
|
||||
if (finalPath != null) {
|
||||
AsyncImage(
|
||||
model = buildCoverImageRequest(
|
||||
context = context,
|
||||
data = finalPath,
|
||||
sourceOrigin = sourceOrigin,
|
||||
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
||||
crossfade = true,
|
||||
),
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onSuccess = {
|
||||
isOnlineCoverLoaded = true
|
||||
isFinalPathLoaded = true
|
||||
onLoadFinish?.invoke()
|
||||
},
|
||||
onError = {
|
||||
isOnlineCoverLoaded = false
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
onLoadFinish?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
Icon(
|
||||
Icons.Default.Book,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(iconSizeDp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||
Icon(
|
||||
Icons.Default.Book,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.secondary,
|
||||
modifier = Modifier
|
||||
.size(iconSizeDp)
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
if (!isOnlineCoverLoaded) {
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
author = author,
|
||||
isNight = isNight
|
||||
)
|
||||
}
|
||||
if (!isOnlineCoverLoaded) {
|
||||
CoverTextOverlay(
|
||||
name = name,
|
||||
author = author,
|
||||
isNight = isNight
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
|
||||
@Composable
|
||||
@@ -42,6 +43,7 @@ fun BookshelfCover(
|
||||
TextCard(
|
||||
text = badgeText,
|
||||
icon = if (showBadgeDot) Icons.Default.Update else null,
|
||||
iconSize = 12.dp,
|
||||
backgroundColor = LegadoTheme.colorScheme.cardContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onCardContainer,
|
||||
modifier = Modifier
|
||||
@@ -49,7 +51,7 @@ fun BookshelfCover(
|
||||
.padding(2.dp),
|
||||
cornerRadius = 4.dp,
|
||||
horizontalPadding = 4.dp,
|
||||
verticalPadding = 0.dp
|
||||
verticalPadding = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,20 +65,17 @@ fun BookshelfCover(
|
||||
.padding(2.dp),
|
||||
cornerRadius = 4.dp,
|
||||
horizontalPadding = 4.dp,
|
||||
verticalPadding = 0.dp
|
||||
verticalPadding = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
if (isUpdating) {
|
||||
LinearProgressIndicator(
|
||||
AppLinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 4.dp, vertical = 6.dp)
|
||||
.height(3.dp),
|
||||
color = LegadoTheme.colorScheme.primary,
|
||||
trackColor = LegadoTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||
strokeCap = StrokeCap.Round
|
||||
.height(3.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.ImageLoader
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@@ -57,16 +56,18 @@ fun Cover(
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(path)
|
||||
.crossfade(true)
|
||||
.setParameter("sourceOrigin", sourceOrigin)
|
||||
.setParameter("loadOnlyWifi", loadOnlyWifi)
|
||||
.listener(
|
||||
model = buildCoverImageRequest(
|
||||
context = context,
|
||||
data = path,
|
||||
sourceOrigin = sourceOrigin,
|
||||
loadOnlyWifi = loadOnlyWifi,
|
||||
crossfade = true,
|
||||
) {
|
||||
listener(
|
||||
onSuccess = { _, _ -> onLoadFinish?.invoke() },
|
||||
onError = { _, _ -> onLoadFinish?.invoke() }
|
||||
)
|
||||
.build(),
|
||||
},
|
||||
imageLoader = imageLoader,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
@@ -93,4 +94,4 @@ fun Cover(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legado.app.ui.widget.components.cover
|
||||
|
||||
import android.content.Context
|
||||
import coil.request.ImageRequest
|
||||
|
||||
fun buildCoverImageRequest(
|
||||
context: Context,
|
||||
data: Any?,
|
||||
sourceOrigin: String?,
|
||||
loadOnlyWifi: Boolean,
|
||||
crossfade: Boolean = true,
|
||||
configure: ImageRequest.Builder.() -> Unit = {},
|
||||
): ImageRequest {
|
||||
return ImageRequest.Builder(context)
|
||||
.data(data)
|
||||
.crossfade(crossfade)
|
||||
.setParameter("sourceOrigin", sourceOrigin)
|
||||
.setParameter("loadOnlyWifi", loadOnlyWifi)
|
||||
.apply(configure)
|
||||
.build()
|
||||
}
|
||||
@@ -5,11 +5,19 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.History
|
||||
import androidx.compose.material.icons.filled.LocationSearching
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.MyLocation
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Replay
|
||||
import androidx.compose.material.icons.filled.RssFeed
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.outlined.Explore
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material.icons.outlined.RssFeed
|
||||
@@ -18,15 +26,23 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import io.legado.app.ui.main.MainDestination
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import okhttp3.internal.http2.Settings
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.basic.Search
|
||||
import top.yukonga.miuix.kmp.icon.extended.Album
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.icon.extended.Close
|
||||
import top.yukonga.miuix.kmp.icon.extended.ContactsBook
|
||||
import top.yukonga.miuix.kmp.icon.extended.Delete
|
||||
import top.yukonga.miuix.kmp.icon.extended.Edit
|
||||
import top.yukonga.miuix.kmp.icon.extended.Favorites
|
||||
import top.yukonga.miuix.kmp.icon.extended.Filter
|
||||
import top.yukonga.miuix.kmp.icon.extended.More
|
||||
import top.yukonga.miuix.kmp.icon.extended.Pin
|
||||
import top.yukonga.miuix.kmp.icon.extended.Refresh
|
||||
import top.yukonga.miuix.kmp.icon.extended.Settings
|
||||
import top.yukonga.miuix.kmp.icon.extended.Unpin
|
||||
import top.yukonga.miuix.kmp.icon.extended.WorldClock
|
||||
|
||||
object AppIcons {
|
||||
|
||||
@@ -38,6 +54,18 @@ object AppIcons {
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search
|
||||
|
||||
val MoreVert: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.More else Icons.Default.MoreVert
|
||||
|
||||
val Edit: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Edit else Icons.Default.Edit
|
||||
|
||||
val Delete: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Delete else Icons.Default.Delete
|
||||
|
||||
val Close: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear
|
||||
@@ -46,6 +74,26 @@ object AppIcons {
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack
|
||||
|
||||
val Filter: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Filter else Icons.Default.FilterList
|
||||
|
||||
val Settings: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Settings else Icons.Default.Settings
|
||||
|
||||
val PrecisionSearch: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Pin else Icons.Default.MyLocation
|
||||
|
||||
val UnPrecisionSearch: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Unpin else Icons.Default.LocationSearching
|
||||
|
||||
val History: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.WorldClock else Icons.Default.History
|
||||
|
||||
val Replay: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
|
||||
|
||||
@@ -45,7 +45,7 @@ fun RoundDropdownMenu(
|
||||
|
||||
if (isMiuix) {
|
||||
val popupContentColor = LegadoTheme.colorScheme.onSurface
|
||||
OverlayListPopup(
|
||||
WindowListPopup(
|
||||
show = expanded,
|
||||
onDismissRequest = onDismissRequest,
|
||||
popupModifier = modifier
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ fun RoundDropdownMenuItem(
|
||||
else -> defaultContentColor.copy(alpha = 0.38f)
|
||||
}
|
||||
}
|
||||
val containerColor = if (isSelected) colorScheme.primaryContainer else colorScheme.surface
|
||||
val containerColor = colorScheme.surface
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
|
||||
@@ -76,7 +76,7 @@ fun <T> RuleListScaffold(
|
||||
show = showDeleteConfirmDialog,
|
||||
onDismissRequest = { showDeleteConfirmDialog = false },
|
||||
title = stringResource(R.string.delete),
|
||||
text = stringResource(R.string.del_msg),
|
||||
text = stringResource(R.string.sure_del),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
onConfirm = {
|
||||
onDeleteSelected(state.selectedIds)
|
||||
|
||||
+7
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -30,6 +31,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||
@@ -41,7 +43,6 @@ import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
|
||||
@Composable
|
||||
fun SliderSettingItem(
|
||||
title: String,
|
||||
color: Color? = null,
|
||||
value: Float,
|
||||
defaultValue: Float,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
@@ -56,7 +57,10 @@ fun SliderSettingItem(
|
||||
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
BasicComponent(
|
||||
title = title,
|
||||
summary = description,
|
||||
@@ -143,7 +147,7 @@ fun SliderSettingItem(
|
||||
} else {
|
||||
SettingItem(
|
||||
title = title,
|
||||
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
option = description,
|
||||
expanded = expanded,
|
||||
onExpandChange = { expanded = it },
|
||||
|
||||
@@ -75,7 +75,7 @@ fun <T> DynamicTopAppBar(
|
||||
if (!isSelecting) {
|
||||
TopBarActionButton(
|
||||
onClick = { onSearchToggle(!state.isSearch) },
|
||||
imageVector = Icons.Default.Search,
|
||||
imageVector = AppIcons.Search,
|
||||
contentDescription = "搜索"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user