[优化] 优化界面

This commit is contained in:
HapeLee committed 2026-04-18 02:25:18 +08:00
1 parent 347995a676
commit c6ed112aa3
60 files changed
+1528 -1452

No files matched your search

@@ -13,6 +13,8 @@ import io.legado.app.data.repository.ExploreRepository
import io.legado.app.data.repository.ExploreRepositoryImpl import io.legado.app.data.repository.ExploreRepositoryImpl
import io.legado.app.data.repository.ReadRecordRepository import io.legado.app.data.repository.ReadRecordRepository
import io.legado.app.data.repository.RemoteBookRepository import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.data.repository.SearchRepository
import io.legado.app.data.repository.SearchRepositoryImpl
import io.legado.app.data.repository.SearchContentRepository import io.legado.app.data.repository.SearchContentRepository
import io.legado.app.data.repository.UploadRepository import io.legado.app.data.repository.UploadRepository
import io.legado.app.domain.usecase.ExploreKindUiUseCase 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.import.remote.RemoteBookViewModel
import io.legado.app.ui.book.info.BookInfoViewModel import io.legado.app.ui.book.info.BookInfoViewModel
import io.legado.app.ui.book.readRecord.ReadRecordViewModel 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.searchContent.SearchContentViewModel
import io.legado.app.ui.book.toc.TocViewModel import io.legado.app.ui.book.toc.TocViewModel
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
@@ -69,6 +72,7 @@ val appModule = module {
single<UploadRepository> { DirectLinkUploadRepository() } single<UploadRepository> { DirectLinkUploadRepository() }
single<ExploreRepository> { ExploreRepositoryImpl(get()) } single<ExploreRepository> { ExploreRepositoryImpl(get()) }
single<SearchRepository> { SearchRepositoryImpl(get()) }
single<ImageLoader> { single<ImageLoader> {
ImageLoader.Builder(get()) ImageLoader.Builder(get())
@@ -111,6 +115,7 @@ val appModule = module {
viewModelOf(::ChangeBookSourceViewModel) viewModelOf(::ChangeBookSourceViewModel)
viewModelOf(::ExploreViewModel) viewModelOf(::ExploreViewModel)
viewModelOf(::RssViewModel) viewModelOf(::RssViewModel)
viewModelOf(::SearchViewModel)
viewModel { (route: ReplaceEditRoute) -> viewModel { (route: ReplaceEditRoute) ->
ReplaceEditViewModel( 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.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding 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.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold 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.SearchBarSection
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem import io.legado.app.ui.widget.components.bookmark.BookmarkItem
@@ -212,7 +211,7 @@ fun AllBookmarkScreen(
) { state -> ) { state ->
when (state) { when (state) {
"LOADING" -> { "LOADING" -> {
EmptyMessageView( EmptyMessage(
message = "加载中...", message = "加载中...",
isLoading = true, isLoading = true,
modifier = Modifier modifier = Modifier
@@ -225,7 +224,7 @@ fun AllBookmarkScreen(
} }
"EMPTY" -> { "EMPTY" -> {
EmptyMessageView( EmptyMessage(
message = "没有书签!", message = "没有书签!",
modifier = Modifier modifier = Modifier
.fillMaxSize() .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.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet 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.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -521,111 +523,12 @@ fun ExploreBookItem(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) SearchBookListItem(
book = book,
val badge: (@Composable RowScope.() -> Unit)? shelfState = shelfState,
= when (shelfState) { onClick = onClick,
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(
modifier = modifier 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 @Composable
@@ -635,71 +538,12 @@ fun ExploreBookGridItem(
shelfState: Flow<BookShelfState>, shelfState: Flow<BookShelfState>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SearchBookGridItem(
val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) book = book,
shelfState = shelfState,
val badgeText: String? = when (shelfState) { onClick = onClick,
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(
modifier = modifier 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) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -8,18 +8,16 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Restore import androidx.compose.material.icons.filled.Restore
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedIconButton import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@@ -34,12 +32,16 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookGroup 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.cover.BookCover
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem 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.settingItem.CompactSwitchSettingItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.FileUtils import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils import io.legado.app.utils.MD5Utils
import io.legado.app.utils.SelectImageContract import io.legado.app.utils.SelectImageContract
@@ -58,10 +60,35 @@ fun GroupEditSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
viewModel: GroupViewModel = koinViewModel() 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( GroupEditContent(
group = group, group = group,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
coverPath = coverPath,
onCoverPathChange = { coverPath = it },
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -71,11 +98,12 @@ fun GroupEditSheet(
fun GroupEditContent( fun GroupEditContent(
group: BookGroup? = null, group: BookGroup? = null,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
coverPath: String?,
onCoverPathChange: (String?) -> Unit,
viewModel: GroupViewModel = koinViewModel() viewModel: GroupViewModel = koinViewModel()
) { ) {
val context = LocalContext.current val context = LocalContext.current
var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") } 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 enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) }
var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) } var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
@@ -96,7 +124,7 @@ fun GroupEditContent(
FileOutputStream(file).use { output -> FileOutputStream(file).use { output ->
context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) } context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) }
} }
coverPath = file.absolutePath onCoverPathChange(file.absolutePath)
} }
} catch (e: Exception) { } catch (e: Exception) {
appCtx.toastOnUi(e.localizedMessage) appCtx.toastOnUi(e.localizedMessage)
@@ -107,6 +135,7 @@ fun GroupEditContent(
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 16.dp)
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
@@ -129,17 +158,22 @@ fun GroupEditContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
OutlinedTextField( AppTextField(
value = groupName, value = groupName,
onValueChange = { groupName = it }, onValueChange = { groupName = it },
label = { AppText(stringResource(R.string.group_name)) }, backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = stringResource(R.string.group_name),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true
) )
GlassCard(
containerColor = LegadoTheme.colorScheme.onSheetContent,
) {
CompactDropdownSettingItem( CompactDropdownSettingItem(
title = stringResource(R.string.sort), title = stringResource(R.string.sort),
selectedValue = selectedSortIndex.toString(), selectedValue = selectedSortIndex.toString(),
color = LegadoTheme.colorScheme.onSheetContent,
displayEntries = sortOptions, displayEntries = sortOptions,
entryValues = sortEntryValues, entryValues = sortEntryValues,
onValueChange = { onValueChange = {
@@ -148,6 +182,7 @@ fun GroupEditContent(
) )
} }
} }
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -159,54 +194,14 @@ fun GroupEditContent(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Row( ConfirmDismissButtonsRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically onDismiss = onDismissRequest,
) { onConfirm = {
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("封面已重置")
}
} 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()) { if (groupName.isEmpty()) {
appCtx.toastOnUi("分组名称不能为空") appCtx.toastOnUi("分组名称不能为空")
} else { } else {
if (group != null) { if (group != null && group.groupId != 0b1L) {
viewModel.upGroup( viewModel.upGroup(
group.copy( group.copy(
groupName = groupName, groupName = groupName,
@@ -228,10 +223,62 @@ fun GroupEditContent(
} }
} }
} }
}) { },
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 package io.legado.app.ui.book.import.local
import android.net.Uri
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -29,7 +28,6 @@ import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.FolderOpen 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.Book
import androidx.compose.material.icons.outlined.Description import androidx.compose.material.icons.outlined.Description
import androidx.compose.material3.CircularProgressIndicator 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.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst 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.ui.config.importBookConfig.ImportBookConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector 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.theme.LegadoTheme
import io.legado.app.ui.widget.components.ActionItem 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.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton 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.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.ConvertUtils 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.startActivityForBook
import io.legado.app.utils.takePersistablePermissionSafely
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import java.io.File
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -222,7 +206,7 @@ private fun ImportBookContent(
} }
state.items.isEmpty() -> { state.items.isEmpty() -> {
EmptyMessageView( EmptyMessage(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
message = stringResource(R.string.empty_msg_import_book) 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.Book
import androidx.compose.material.icons.outlined.CloudSync import androidx.compose.material.icons.outlined.CloudSync
import androidx.compose.material.icons.outlined.Description 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.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme 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.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable 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.ActionItem
import io.legado.app.ui.widget.components.AppRadioButton import io.legado.app.ui.widget.components.AppRadioButton
import io.legado.app.ui.widget.components.AppTextField 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.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.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.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.card.TextCard 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.ConvertUtils import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.FileDoc
import io.legado.app.utils.isUri import io.legado.app.utils.isUri
import io.legado.app.utils.startActivityForBook import io.legado.app.utils.startActivityForBook
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
@@ -173,9 +170,10 @@ fun RemoteBookScreen(
}, },
endAction = if (showSheet is RemoteBookSheet.Servers) { endAction = if (showSheet is RemoteBookSheet.Servers) {
{ {
IconButton(onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }) { MediumIconButton(
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add)) onClick = { showSheet = RemoteBookSheet.ServerConfig(null) },
} imageVector = Icons.Default.Add
)
} }
} else { } else {
null null
@@ -265,9 +263,10 @@ fun RemoteBookScreen(
onSearchQueryChange = { viewModel.dispatch(RemoteBookIntent.SearchChange(it)) }, onSearchQueryChange = { viewModel.dispatch(RemoteBookIntent.SearchChange(it)) },
searchPlaceholder = "搜索", searchPlaceholder = "搜索",
topBarActions = { topBarActions = {
IconButton(onClick = { showSheet = RemoteBookSheet.Servers }) { TopBarActionButton(
Icon(Icons.Default.Storage, contentDescription = "配置服务器") onClick = { showSheet = RemoteBookSheet.Servers },
} imageVector = Icons.Default.Storage
)
}, },
dropDownMenuContent = { dismiss -> dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem( RoundDropdownMenuItem(
@@ -335,7 +334,7 @@ fun RemoteBookScreen(
if (uiState.isLoading) { if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else { } else {
EmptyMessageView( EmptyMessage(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
@@ -453,7 +452,7 @@ private fun ServerItem(
} else { } else {
null null
}, },
containerColor = LegadoTheme.colorScheme.surface, containerColor = LegadoTheme.colorScheme.onSheetContent,
selectedContainerColor = LegadoTheme.colorScheme.secondaryContainer, selectedContainerColor = LegadoTheme.colorScheme.secondaryContainer,
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -483,6 +482,7 @@ private fun ServerConfigSheetContent(
AppTextField( AppTextField(
value = name, value = name,
onValueChange = { name = it }, onValueChange = { name = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = "名称", label = "名称",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true
@@ -491,6 +491,7 @@ private fun ServerConfigSheetContent(
AppTextField( AppTextField(
value = url, value = url,
onValueChange = { url = it }, onValueChange = { url = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = "URL", label = "URL",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true
@@ -499,6 +500,7 @@ private fun ServerConfigSheetContent(
AppTextField( AppTextField(
value = username, value = username,
onValueChange = { username = it }, onValueChange = { username = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = "用户名", label = "用户名",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true
@@ -507,6 +509,7 @@ private fun ServerConfigSheetContent(
AppTextField( AppTextField(
value = password, value = password,
onValueChange = { password = it }, onValueChange = { password = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = "密码", label = "密码",
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
@@ -523,18 +526,10 @@ private fun ServerConfigSheetContent(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Row( ConfirmDismissButtonsRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) onDismiss = onCancel,
) { onConfirm = {
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 newServer = server?.copy(name = name) ?: Server(name = name)
val newConfig = JSONObject().apply { val newConfig = JSONObject().apply {
put("url", url) put("url", url)
@@ -544,12 +539,10 @@ private fun ServerConfigSheetContent(
newServer.config = newConfig.toString() newServer.config = newConfig.toString()
onSave(newServer) onSave(newServer)
}, },
modifier = Modifier.weight(1f), dismissText = stringResource(android.R.string.cancel),
enabled = name.isNotBlank() && url.isNotBlank() confirmText = "保存",
) { confirmEnabled = name.isNotBlank() && url.isNotBlank()
AppText("保存") )
}
}
} }
} }
@@ -3,7 +3,6 @@ package io.legado.app.ui.book.info
import android.net.Uri import android.net.Uri
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable 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.Share
import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.outlined.Book 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.CollectionsBookmark
import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Image 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.CircularProgressIndicator
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MediumFlexibleTopAppBar
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@@ -89,6 +91,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.ImageLoader import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.BookType
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter 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.book.source.manage.BookSourceActivity
import io.legado.app.ui.config.coverConfig.CoverConfig import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.theme.LegadoTheme 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.ProvideThemeOverride
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.ThemeOverrideState import io.legado.app.ui.theme.ThemeOverrideState
import io.legado.app.ui.theme.rememberImageSeedColor import io.legado.app.ui.theme.rememberImageSeedColor
import io.legado.app.ui.theme.rememberThemeOverride import io.legado.app.ui.theme.rememberThemeOverride
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.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.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet 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.AnimatedTextLine
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassTopAppBarScrollBehavior
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults 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.ui.widget.components.topbar.MiuixGlassScrollBehavior
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
@@ -134,6 +142,7 @@ import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -162,32 +171,12 @@ private fun BookInfoScreenContent(
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val miuixScrollBehavior = if (isMiuix) { val scrollBehavior = if (isMiuix) {
GlassTopAppBarDefaults.defaultScrollBehavior() as? MiuixGlassScrollBehavior MiuixGlassScrollBehavior(MiuixScrollBehavior())
} else { } else {
null M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior())
} }
val miuixCollapsedFraction = miuixScrollBehavior?.collapsedFraction ?: 0f
val listState = rememberLazyListState() 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() val pullState = rememberPullToRefreshState()
var showMenu by rememberSaveable { mutableStateOf(false) } var showMenu by rememberSaveable { mutableStateOf(false) }
@@ -196,64 +185,17 @@ private fun BookInfoScreenContent(
AppScaffold( AppScaffold(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.then( .nestedScroll(scrollBehavior.nestedScrollConnection),
if (miuixScrollBehavior != null) {
Modifier.nestedScroll(miuixScrollBehavior.nestedScrollConnection)
} else {
Modifier
}
),
topBar = { topBar = {
if (isMiuix) { BookInfoTransparentTopAppBar(
MiuixTopAppBar(
title = "",
color = appBarColor,
navigationIcon = {
TopBarNavigationButton(
onClick = { onIntent(BookInfoIntent.BackPressed) }
)
},
actions = {
BookInfoTopBarActions(
state = state, state = state,
showMenu = showMenu, showMenu = showMenu,
onShowMenuChange = { showMenu = it }, onShowMenuChange = { showMenu = it },
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) }, onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
onBackPressed = { onIntent(BookInfoIntent.BackPressed) },
scrollBehavior = scrollBehavior,
) )
}, },
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,
),
)
}
},
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( ExtendedFloatingActionButton(
onClick = { onIntent(BookInfoIntent.ReadClick) }, onClick = { onIntent(BookInfoIntent.ReadClick) },
@@ -322,6 +264,7 @@ private fun BookInfoScreenContent(
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
BookInfoActions( BookInfoActions(
book = book,
inBookshelf = state.inBookshelf, inBookshelf = state.inBookshelf,
onShelfClick = { onIntent(BookInfoIntent.ShelfClick) }, onShelfClick = { onIntent(BookInfoIntent.ShelfClick) },
onTocClick = { onIntent(BookInfoIntent.TocClick) }, onTocClick = { onIntent(BookInfoIntent.TocClick) },
@@ -416,6 +359,70 @@ private fun BookInfoColorTheme(
ProvideThemeOverride(theme = theme, content = content) 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 @Composable
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? { private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
val imageLoader = koinInject<ImageLoader>() val imageLoader = koinInject<ImageLoader>()
@@ -470,6 +477,19 @@ private fun BookInfoTopBarActions(
@Composable @Composable
private fun BookInfoBackdrop(book: Book) { private fun BookInfoBackdrop(book: Book) {
val cover = book.getDisplayCover() 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( val seedOverlay = lerp(
LegadoTheme.colorScheme.secondaryContainer, LegadoTheme.colorScheme.secondaryContainer,
LegadoTheme.seedColor, LegadoTheme.seedColor,
@@ -478,7 +498,8 @@ private fun BookInfoBackdrop(book: Book) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
if (!cover.isNullOrBlank()) { if (!cover.isNullOrBlank()) {
AsyncImage( AsyncImage(
model = cover, model = backdropRequest,
imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -567,22 +588,24 @@ private fun BookInfoOverflowMenu(
text = stringResource(R.string.to_top), text = stringResource(R.string.to_top),
onClick = { onMenuAction(BookInfoMenuAction.Top) } onClick = { onMenuAction(BookInfoMenuAction.Top) }
) )
if (book?.isLocal == false ){
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.allow_update), text = stringResource(R.string.allow_update),
onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) }, onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) } isSelected = book.canUpdate
) )
if (book?.isLocal == true && book.type and io.legado.app.constant.BookType.text > 0) { }
if (book?.isLocal == true && book.type and BookType.text > 0) {
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.split_long_chapter), text = stringResource(R.string.split_long_chapter),
onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) }, onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) },
trailingIcon = { if (book.getSplitLongChapter()) Icon(Icons.Default.Star, null) } isSelected = book.getSplitLongChapter()
) )
} }
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.delete_alert), text = stringResource(R.string.delete_alert),
onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) }, 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( RoundDropdownMenuItem(
text = stringResource(R.string.clear_cache), text = stringResource(R.string.clear_cache),
@@ -606,11 +629,6 @@ private fun BookInfoHeader(
onBookNameClick: (Boolean) -> Unit, onBookNameClick: (Boolean) -> Unit,
onOriginClick: () -> Unit, onOriginClick: () -> Unit,
) { ) {
val labelBackground = lerp(
LegadoTheme.colorScheme.surface,
LegadoTheme.seedColor,
0.1f
).copy(alpha = 0.82f)
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -688,6 +706,16 @@ private fun BookInfoHeader(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), 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( itemsIndexed(
items = kindLabels, items = kindLabels,
key = { index, label -> "kind-$index-$label" } key = { index, label -> "kind-$index-$label" }
@@ -695,20 +723,10 @@ private fun BookInfoHeader(
TextCard( TextCard(
text = label, text = label,
textStyle = LegadoTheme.typography.labelLargeEmphasized, textStyle = LegadoTheme.typography.labelLargeEmphasized,
backgroundColor = labelBackground, backgroundColor = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onSurface, 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 @Composable
private fun BookInfoActions( private fun BookInfoActions(
book: Book,
inBookshelf: Boolean, inBookshelf: Boolean,
onShelfClick: () -> Unit, onShelfClick: () -> Unit,
onTocClick: () -> Unit, onTocClick: () -> Unit,
@@ -747,7 +766,7 @@ private fun BookInfoActions(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(LegadoTheme.colorScheme.surface) .background(LegadoTheme.colorScheme.surface)
.padding(horizontal = 16.dp, vertical = 4.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
BookInfoActionCard( BookInfoActionCard(
@@ -772,7 +791,7 @@ private fun BookInfoActions(
) )
BookInfoActionCard( BookInfoActionCard(
modifier = Modifier.weight(1f), 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), label = stringResource(R.string.change_group),
onClick = onGroupClick onClick = onGroupClick
) )
@@ -788,14 +807,14 @@ private fun BookInfoActions(
@Composable @Composable
private fun BookInfoActionCard( private fun BookInfoActionCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: ImageVector,
label: String, label: String,
onClick: () -> Unit onClick: () -> Unit
) { ) {
GlassCard( GlassCard(
modifier = modifier, modifier = modifier,
onClick = onClick, onClick = onClick,
containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f), containerColor = LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.2f),
contentColor = LegadoTheme.colorScheme.onSurface, contentColor = LegadoTheme.colorScheme.onSurface,
) { ) {
Column( Column(
@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells 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.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.theme.LegadoTheme 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.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
@@ -134,7 +136,11 @@ fun GroupSelectSheet(
title = stringResource(R.string.group_select), title = stringResource(R.string.group_select),
endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } } endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } }
) { ) {
LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(modifier = Modifier.fillMaxWidth()) {
LazyColumn(
modifier = Modifier.heightIn(max = 600.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(groups, key = { it.groupId }) { group -> items(groups, key = { it.groupId }) { group ->
val isSelected = selectedGroupId and group.groupId > 0 val isSelected = selectedGroupId and group.groupId > 0
SelectionItemCard( SelectionItemCard(
@@ -178,9 +184,10 @@ fun GroupSelectSheet(
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
} }
}
GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null }) GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null })
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ChangeCoverSheet( fun ChangeCoverSheet(
@@ -376,7 +383,7 @@ fun ChangeSourceSheet(
) )
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
if (isSearching) { if (isSearching) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
AppText( AppText(
text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}", 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.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.CollapsibleHeader import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.AppIconButton import io.legado.app.ui.widget.components.button.AppIconButton
@@ -248,7 +247,7 @@ fun ReadRecordScreen(
) { targetState -> ) { targetState ->
when (targetState) { when (targetState) {
"LOADING" -> { "LOADING" -> {
EmptyMessageView( EmptyMessage(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding( .padding(
@@ -261,7 +260,7 @@ fun ReadRecordScreen(
} }
"EMPTY" -> { "EMPTY" -> {
EmptyMessageView( EmptyMessage(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding( .padding(
@@ -1,674 +1,73 @@
package io.legado.app.ui.book.search package io.legado.app.ui.book.search
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import androidx.compose.runtime.Composable
import android.view.Menu import io.legado.app.base.BaseComposeActivity
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 io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity 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.startActivity
import io.legado.app.utils.transaction import org.koin.androidx.viewmodel.ext.android.viewModel
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
class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel>(), class SearchActivity : BaseComposeActivity() {
BookAdapter.CallBack,
HistoryKeyAdapter.CallBack,
SearchScopeDialog.Callback,
SearchAdapter.CallBack {
override val binding by viewBinding(ActivityBookSearchBinding::inflate) private val viewModel by viewModel<SearchViewModel>()
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
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setSupportActionBar(binding.searchBar) dispatchInit(intent)
initRecyclerView()
initMaterialSearch()
initOtherView()
initData()
updateSelectedGroup()
receiptIntent(intent)
} }
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
receiptIntent(intent) setIntent(intent)
dispatchInit(intent)
} }
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { override fun onResume() {
menuInflater.inflate(R.menu.book_search, menu) super.onResume()
this.menu = menu viewModel.onIntent(SearchIntent.ResumeEngine)
precisionSearchMenuItem = menu.findItem(R.id.menu_precision_search)
precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch)
return super.onCompatCreateOptionsMenu(menu)
} }
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { override fun onPause() {
buildSearchScopeMenu(menu) viewModel.onIntent(SearchIntent.PauseEngine)
updateSearchScopeChip() super.onPause()
return super.onMenuOpened(featureId, menu)
} }
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { @Composable
when (item.itemId) { override fun Content() {
R.id.menu_precision_search -> { SearchScreen(
val newValue = !getPrefBoolean(PreferKey.precisionSearch) viewModel = viewModel,
putPrefBoolean(PreferKey.precisionSearch, newValue) onBack = { finish() },
precisionSearchMenuItem?.isChecked = newValue onOpenBookInfo = { name, author, bookUrl ->
binding.chipPrecisionSearch.isChecked = newValue
binding.searchView.text.toString().trim().let {
binding.searchView.setText(it)
}
}
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> { startActivity<BookInfoActivity> {
putExtra("name", name) putExtra("name", name)
putExtra("author", author) putExtra("author", author)
putExtra("bookUrl", bookUrl) putExtra("bookUrl", bookUrl)
} }
},
onOpenSourceManage = {
startActivity<BookSourceActivity>()
}
)
} }
/** private fun dispatchInit(intent: Intent?) {
* 是否已经加入书架 viewModel.onIntent(
*/ SearchIntent.Initialize(
override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState { key = intent?.getStringExtra("key"),
return viewModel.isInBookShelf(name, author, url) scopeRaw = intent?.getStringExtra("searchScope"),
} )
)
/**
* 显示书籍详情
*/
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()
} }
companion object { companion object {
fun start(context: Context, key: String?, searchScope: String? = null) { fun start(context: Context, key: String?, searchScope: String? = null) {
context.startActivity<SearchActivity> { context.startActivity<SearchActivity> {
putExtra("key", key) putExtra("key", key)
putExtra("searchScope", searchScope) putExtra("searchScope", searchScope)
} }
} }
} }
} }
@@ -18,11 +18,11 @@ data class SearchScope(private var scope: String) {
constructor(groups: List<String>) : this(groups.joinToString(",")) constructor(groups: List<String>) : this(groups.joinToString(","))
constructor(source: BookSource) : this( constructor(source: BookSource) : this(
"${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
) )
constructor(source: BookSourcePart) : this( constructor(source: BookSourcePart) : this(
"${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" encodeSourceToken(source.bookSourceName, source.bookSourceUrl)
) )
override fun toString(): String { override fun toString(): String {
@@ -44,19 +44,35 @@ data class SearchScope(private var scope: String) {
} }
fun update(source: BookSource) { 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) stateLiveData.postValue(scope)
save() save()
} }
fun isSource(): Boolean { fun isSource(): Boolean {
return scope.contains("::") val items = scopeItems()
if (items.isEmpty()) return false
return parseSourceItems(items).size == items.size
} }
val display: String val display: String
get() { get() {
if (scope.contains("::")) { if (isSource()) {
return scope.substringBefore("::") val sourceNames = parseSourceItems().map { it.name }
if (sourceNames.isEmpty()) return appCtx.getString(R.string.all_source)
return sourceNames.joinToString(",")
} }
if (scope.isEmpty()) { if (scope.isEmpty()) {
return appCtx.getString(R.string.all_source) return appCtx.getString(R.string.all_source)
@@ -70,22 +86,30 @@ data class SearchScope(private var scope: String) {
val displayNames: List<String> val displayNames: List<String>
get() { get() {
val list = arrayListOf<String>() val list = arrayListOf<String>()
if (scope.contains("::")) { if (isSource()) {
list.add(scope.substringBefore("::")) parseSourceItems().forEach {
list.add(it.name)
}
} else { } else {
scope.splitNotBlank(",").forEach { scopeItems().forEach {
list.add(it) list.add(it)
} }
} }
return list return list
} }
val sourceUrls: List<String>
get() = parseSourceItems().map { it.url }
fun remove(scope: String) { fun remove(scope: String) {
if (isSource()) { if (isSource()) {
this.scope = "" val sourceItems = parseSourceItems().filterNot {
it.name == scope || it.url == scope
}
this.scope = sourceItems.joinToString(",") { "${it.name}::${it.url}" }
} else { } else {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
this.scope.split(",").forEach { scopeItems().forEach {
if (it != scope) { if (it != scope) {
if (stringBuilder.isNotEmpty()) { if (stringBuilder.isNotEmpty()) {
stringBuilder.append(",") stringBuilder.append(",")
@@ -106,14 +130,14 @@ data class SearchScope(private var scope: String) {
if (scope.isEmpty()) { if (scope.isEmpty()) {
list.addAll(appDb.bookSourceDao.allEnabledPart) list.addAll(appDb.bookSourceDao.allEnabledPart)
} else { } else {
if (scope.contains("::")) { if (isSource()) {
scope.substringAfter("::").let { parseSourceItems().forEach { sourceItem ->
appDb.bookSourceDao.getBookSourcePart(it)?.let { source -> appDb.bookSourceDao.getBookSourcePart(sourceItem.url)?.let { source ->
list.add(source) list.add(source)
} }
} }
} else { } else {
val oldScope = scope.splitNotBlank(",") val oldScope = scopeItems()
val newScope = oldScope.filter { val newScope = oldScope.filter {
val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it) val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it)
list.addAll(bookSources) 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 package io.legado.app.ui.book.search
import android.app.Application import androidx.lifecycle.ViewModel
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel import io.legado.app.constant.PreferKey
import io.legado.app.constant.AppLog import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.appDb
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.repository.SearchRepository
import io.legado.app.help.book.isNotShelf import io.legado.app.data.repository.SearchSessionEvent
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState import io.legado.app.model.BookShelfState
import io.legado.app.model.webBook.SearchModel import io.legado.app.ui.main.bookshelf.BookShelfItem
import io.legado.app.utils.ConflateLiveData import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.toastOnUi import io.legado.app.utils.putPrefBoolean
import kotlinx.coroutines.ExperimentalCoroutinesApi 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.catch
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.distinctUntilChanged
import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
data class BookKey(val name: String, val author: String, val url: String?) import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import splitties.init.appCtx
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SearchViewModel(application: Application) : BaseViewModel(application) { class SearchViewModel(
val handler = Handler(Looper.getMainLooper()) private val repository: SearchRepository,
val bookshelf: MutableSet<BookKey> = ConcurrentHashMap.newKeySet() ) : ViewModel() {
val upAdapterLiveData = MutableLiveData<String>()
var searchBookLiveData = ConflateLiveData<List<SearchBook>>(1000)
val searchScope: SearchScope = SearchScope(AppConfig.searchScope)
var searchFinishLiveData = MutableLiveData<Boolean>()
var isSearchLiveData = MutableLiveData<Boolean>()
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 = "" private val _effects = MutableSharedFlow<SearchEffect>(extraBufferCapacity = 16)
var hasMore = true val effects = _effects.asSharedFlow()
private var searchID = 0L
private val searchModel = SearchModel(viewModelScope, object : SearchModel.CallBack {
override fun getSearchScope(): SearchScope { private val queryFlow = MutableStateFlow("")
return searchScope private val bookshelfKeys = MutableStateFlow<Set<BookKey>>(emptySet())
} private val searchScope = SearchScope(AppConfig.searchScope)
private val searchSession = repository.createSearchSession { searchScope }
override fun onSearchStart() { private var currentSearchId = 0L
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)
}
}
})
init { init {
execute { syncScopeState()
appDb.bookDao.flowAll().mapLatest { books -> observeEnabledGroups()
books.filterNot { it.isNotShelf } observeEnabledSources()
}.catch { observeBookshelf()
AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it) observeQueryHistory()
}.collect { books -> observeQueryBookshelfHints()
bookshelf.clear() observeSearchSession()
bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) })
upAdapterLiveData.postValue("isInBookshelf")
} }
}.onError {
AppLog.put("加载书架数据失败", it) 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)
}
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()
} }
} }
fun isInBookShelf(name: String, author: String, url: String?): BookShelfState { is SearchIntent.SetScopeSheetVisible -> {
val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url } _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 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 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 if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF return BookShelfState.NOT_IN_SHELF
} }
/** private fun emitEffect(effect: SearchEffect) {
* 开始搜索 _effects.tryEmit(effect)
*/
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)
}
}
/**
* 停止搜索
*/
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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -42,8 +40,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect 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.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold 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.SearchBarSection
import io.legado.app.ui.widget.components.button.MediumOutlinedButton 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.SmallAnimatedActionButton
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton
@@ -225,7 +220,7 @@ fun SearchContentScreen(
) { state -> ) { state ->
when (state) { when (state) {
is SearchContentState.Error -> { is SearchContentState.Error -> {
EmptyMessageView( EmptyMessage(
message = state.throwable.localizedMessage ?: "发生未知错误", message = state.throwable.localizedMessage ?: "发生未知错误",
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -244,7 +239,7 @@ fun SearchContentScreen(
) )
} }
SearchContentState.EmptyResult -> { SearchContentState.EmptyResult -> {
EmptyMessageView( EmptyMessage(
message = "没有找到相关内容!", message = "没有找到相关内容!",
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -319,7 +314,7 @@ fun SearchHistoryList(
} }
if (history.isEmpty()) { if (history.isEmpty()) {
EmptyMessageView( EmptyMessage(
message = "暂无搜索历史", message = "暂无搜索历史",
modifier = Modifier modifier = Modifier
.fillMaxSize() .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.CollapsibleHeader
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AppScaffold 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.SelectionBottomBar
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem import io.legado.app.ui.widget.components.bookmark.BookmarkItem
@@ -864,7 +864,7 @@ fun BookmarkListContent(
), ),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
EmptyMessageView( EmptyMessage(
message = "暂无书签" 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.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet 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.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
@@ -204,17 +205,17 @@ fun TxtRuleScreen(
} }
AppAlertDialog( AppAlertDialog(
data = showDeleteRuleDialog, // 传入 nullable �rule 对象 data = showDeleteRuleDialog,
onDismissRequest = { showDeleteRuleDialog = null }, onDismissRequest = { showDeleteRuleDialog = null },
title = stringResource(R.string.delete), title = stringResource(R.string.delete),
text = stringResource(R.string.sure_del),
confirmText = stringResource(R.string.ok), confirmText = stringResource(R.string.ok),
onConfirm = { rule -> onConfirm = { rule ->
viewModel.delete(rule) viewModel.delete(rule)
showDeleteRuleDialog = null showDeleteRuleDialog = null
}, },
dismissText = stringResource(R.string.cancel), dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteRuleDialog = null }, onDismiss = { showDeleteRuleDialog = null }
content = { Text(text = stringResource(R.string.del_msg)) }
) )
RuleEditSheet( RuleEditSheet(
@@ -228,7 +229,7 @@ fun TxtRuleScreen(
editingRule = null editingRule = null
}, },
onSave = { updatedRule -> onSave = { updatedRule ->
//TODO:我很想把他改为自增主键,但为了兼容性日å�Žå†�è¯? //TODO:我很想把他改为自增主键,但为了兼容性日后再�?
if (editingRule == null) { if (editingRule == null) {
viewModel.insert(updatedRule) viewModel.insert(updatedRule)
} else { } else {
@@ -346,7 +347,7 @@ fun TxtRuleScreen(
trailingAction = { trailingAction = {
SmallIconButton( SmallIconButton(
onClick = { showDeleteRuleDialog = item.rule }, 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 androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme 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.CheckboxGroupContainer
import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -36,33 +38,31 @@ fun CheckSourceBottomSheet(
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismiss onDismissRequest = onDismiss,
title = stringResource(R.string.check_source_config)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 32.dp) .padding(bottom = 32.dp)
) { ) {
AppText(
text = stringResource(R.string.check_source_config),
style = LegadoTheme.typography.titleLarge,
modifier = Modifier.padding(vertical = 16.dp)
)
GlassCard {
SliderSettingItem( SliderSettingItem(
title = stringResource(R.string.check_source_timeout), title = stringResource(R.string.check_source_timeout),
color = MaterialTheme.colorScheme.surface,
value = viewModel.checkSourceTimeout.toFloat(), value = viewModel.checkSourceTimeout.toFloat(),
defaultValue = 180f, defaultValue = 180f,
onValueChange = { viewModel.checkSourceTimeout = it.toLong() }, onValueChange = { viewModel.checkSourceTimeout = it.toLong() },
valueRange = 0f..300f, valueRange = 0f..300f,
) )
}
Spacer(modifier = Modifier.padding(8.dp)) Spacer(modifier = Modifier.padding(8.dp))
CheckboxGroupContainer(columns = 2) { Column(
item { verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CheckboxItem( CheckboxItem(
title = stringResource(R.string.search), title = stringResource(R.string.search),
checked = viewModel.checkSearch, checked = viewModel.checkSearch,
@@ -73,9 +73,7 @@ fun CheckSourceBottomSheet(
} }
} }
) )
}
item {
CheckboxItem( CheckboxItem(
title = stringResource(R.string.discovery), title = stringResource(R.string.discovery),
checked = viewModel.checkDiscovery, checked = viewModel.checkDiscovery,
@@ -86,12 +84,9 @@ fun CheckSourceBottomSheet(
} }
} }
) )
}
}
CheckboxGroupContainer(columns = 3) {
item {
CheckboxItem( CheckboxItem(
title = stringResource(R.string.source_tab_info), title = stringResource(R.string.source_tab_info),
checked = viewModel.checkInfo, checked = viewModel.checkInfo,
@@ -103,9 +98,8 @@ fun CheckSourceBottomSheet(
} }
} }
) )
}
item {
CheckboxItem( CheckboxItem(
title = stringResource(R.string.chapter_list), title = stringResource(R.string.chapter_list),
checked = viewModel.checkCategory, checked = viewModel.checkCategory,
@@ -115,9 +109,8 @@ fun CheckSourceBottomSheet(
if (!it) viewModel.checkContent = false if (!it) viewModel.checkContent = false
} }
) )
}
item {
CheckboxItem( CheckboxItem(
title = stringResource(R.string.source_tab_content), title = stringResource(R.string.source_tab_content),
checked = viewModel.checkContent, checked = viewModel.checkContent,
@@ -125,27 +118,22 @@ fun CheckSourceBottomSheet(
onCheckedChange = { viewModel.checkContent = it } onCheckedChange = { viewModel.checkContent = it }
) )
} }
}
Row( ConfirmDismissButtonsRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp), .padding(top = 16.dp),
horizontalArrangement = Arrangement.End onDismiss = onDismiss,
) { onConfirm = {
TextButton(onClick = onDismiss) {
AppText(stringResource(R.string.cancel))
}
Button(onClick = {
if (viewModel.saveCheckSourceConfig()) { if (viewModel.saveCheckSourceConfig()) {
onDismiss() onDismiss()
} else { } else {
Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show() 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)
} )
} }
} }
} }
@@ -1,5 +1,6 @@
package io.legado.app.ui.config.otherConfig package io.legado.app.ui.config.otherConfig
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Checklist
import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.ContentPaste import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Download 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.R
import io.legado.app.help.DirectLinkUpload import io.legado.app.help.DirectLinkUpload
import io.legado.app.lib.dialogs.selector 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.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
import io.legado.app.ui.widget.components.button.MediumIconButton 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.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -65,7 +69,16 @@ fun DirectLinkUploadBottomSheet(
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
title = stringResource(R.string.direct_link_upload_config), title = stringResource(R.string.direct_link_upload_config),
startAction = {
MediumIconButton(
onClick = {
viewModel.testRule { result -> showTestResult = result }
},
imageVector = Icons.Default.Checklist
)
},
endAction = { endAction = {
Box {
MediumIconButton( MediumIconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert imageVector = Icons.Default.MoreVert
@@ -116,6 +129,8 @@ fun DirectLinkUploadBottomSheet(
} }
) )
} }
}
}, },
onDismissRequest = onDismiss onDismissRequest = onDismiss
) { ) {
@@ -128,6 +143,7 @@ fun DirectLinkUploadBottomSheet(
AppTextField( AppTextField(
value = viewModel.uploadUrl, value = viewModel.uploadUrl,
onValueChange = { viewModel.uploadUrl = it }, onValueChange = { viewModel.uploadUrl = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = stringResource(R.string.upload_url), label = stringResource(R.string.upload_url),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@@ -137,6 +153,7 @@ fun DirectLinkUploadBottomSheet(
AppTextField( AppTextField(
value = viewModel.downloadUrlRule, value = viewModel.downloadUrlRule,
onValueChange = { viewModel.downloadUrlRule = it }, onValueChange = { viewModel.downloadUrlRule = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = stringResource(R.string.download_url_rule), label = stringResource(R.string.download_url_rule),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@@ -146,6 +163,7 @@ fun DirectLinkUploadBottomSheet(
AppTextField( AppTextField(
value = viewModel.summary, value = viewModel.summary,
onValueChange = { viewModel.summary = it }, onValueChange = { viewModel.summary = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = stringResource(R.string.summary), label = stringResource(R.string.summary),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
@@ -154,33 +172,26 @@ fun DirectLinkUploadBottomSheet(
CheckboxItem( CheckboxItem(
title = stringResource(R.string.is_compress), title = stringResource(R.string.is_compress),
color = MaterialTheme.colorScheme.surface, color = LegadoTheme.colorScheme.onSheetContent,
checked = viewModel.compress, checked = viewModel.compress,
onCheckedChange = { viewModel.compress = it } onCheckedChange = { viewModel.compress = it }
) )
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Row(modifier = Modifier.fillMaxWidth()) { ConfirmDismissButtonsRow(
TextButton(onClick = { modifier = Modifier.fillMaxWidth(),
viewModel.testRule { result -> showTestResult = result } onDismiss = onDismiss,
}) { onConfirm = {
AppText("测试")
}
Spacer(Modifier.weight(1f))
OutlinedButton(onClick = onDismiss) {
AppText(stringResource(R.string.cancel))
}
Button(onClick = {
if (viewModel.saveDirectLinkRule()) { if (viewModel.saveDirectLinkRule()) {
onDismiss() onDismiss()
} else { } else {
context.toastOnUi("请填写完整信息") 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 androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -35,24 +37,22 @@ fun PageKeySheet(
var prevKeys by remember { mutableStateOf(ReadConfig.prevKeys) } var prevKeys by remember { mutableStateOf(ReadConfig.prevKeys) }
var nextKeys by remember { mutableStateOf(ReadConfig.nextKeys) } 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( Column(
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
AppText( AppTextField(
text = stringResource(R.string.custom_page_key),
style = LegadoTheme.typography.titleLarge,
fontWeight = FontWeight.Companion.Bold
)
OutlinedTextField(
value = prevKeys, value = prevKeys,
onValueChange = { prevKeys = it }, onValueChange = { prevKeys = it },
label = { AppText(stringResource(R.string.prev_page_key)) }, label = stringResource(R.string.prev_page_key),
modifier = Modifier.Companion modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.onPreviewKeyEvent { event -> .onPreviewKeyEvent { event ->
if (event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) { if (event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) {
@@ -71,10 +71,10 @@ fun PageKeySheet(
singleLine = true singleLine = true
) )
OutlinedTextField( AppTextField(
value = nextKeys, value = nextKeys,
onValueChange = { nextKeys = it }, onValueChange = { nextKeys = it },
label = { AppText(stringResource(R.string.next_page_key)) }, label = stringResource(R.string.next_page_key),
modifier = Modifier.Companion modifier = Modifier.Companion
.fillMaxWidth() .fillMaxWidth()
.onPreviewKeyEvent { event -> .onPreviewKeyEvent { event ->
@@ -96,35 +96,23 @@ fun PageKeySheet(
AppText( AppText(
text = stringResource(R.string.page_key_set_help), text = stringResource(R.string.page_key_set_help),
style = LegadoTheme.typography.bodyMedium, style = LegadoTheme.typography.bodyMedium
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Row( ConfirmDismissButtonsRow(
modifier = Modifier.Companion.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp) onDismiss = {
) {
OutlinedButton(
onClick = {
prevKeys = "" prevKeys = ""
nextKeys = "" nextKeys = ""
}, },
modifier = Modifier.Companion.weight(1f) onConfirm = {
) {
AppText(stringResource(R.string.reset))
}
Button(
onClick = {
ReadConfig.prevKeys = prevKeys ReadConfig.prevKeys = prevKeys
ReadConfig.nextKeys = nextKeys ReadConfig.nextKeys = nextKeys
onDismissRequest() onDismissRequest()
}, },
modifier = Modifier.Companion.weight(1f) dismissText = stringResource(R.string.reset),
) { confirmText = stringResource(R.string.ok)
AppText(stringResource(R.string.ok)) )
}
}
} }
} }
} }
@@ -31,7 +31,9 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme 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.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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -68,35 +70,31 @@ fun BackgroundImageManageSheet(
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismissRequest onDismissRequest = onDismissRequest,
title = stringResource(R.string.background_image),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 24.dp) .padding(bottom = 24.dp)
) { ) {
AppText(
text = stringResource(R.string.background_image),
style = LegadoTheme.typography.titleMedium,
modifier = Modifier.padding(16.dp)
)
if (currentPath.isNullOrBlank()) { if (currentPath.isNullOrBlank()) {
Surface( Surface(
onClick = { showFilePicker = true }, onClick = { showFilePicker = true },
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = LegadoTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(16f / 9f) .aspectRatio(16f / 9f)
.padding(16.dp) .padding(16.dp)
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Icon( AppIcon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
contentDescription = "Add", contentDescription = "Add",
modifier = Modifier.size(48.dp), modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary tint = LegadoTheme.colorScheme.primary
) )
} }
} }
@@ -119,28 +117,18 @@ fun BackgroundImageManageSheet(
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
) )
} }
IconButton( SmallTonalIconButton(
onClick = { viewModel.removeBackground(isDarkTheme) }, onClick = { viewModel.removeBackground(isDarkTheme) },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(8.dp) .padding(8.dp)
.size(32.dp) .size(32.dp),
) { imageVector = Icons.Default.Close
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Remove",
modifier = Modifier.size(20.dp)
) )
} }
} }
} }
} }
}
}
FilePickerSheet( FilePickerSheet(
show = showFilePicker, show = showFilePicker,
@@ -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.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet 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.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
@@ -320,7 +321,7 @@ fun DictRuleScreen(
trailingAction = { trailingAction = {
SmallIconButton( SmallIconButton(
onClick = { showDeleteRuleDialog = item.rule }, onClick = { showDeleteRuleDialog = item.rule },
imageVector = Icons.Default.Delete imageVector = AppIcons.Delete
) )
} }
) )
@@ -376,6 +376,7 @@ fun BookGroupItemList(
fun BookItem( fun BookItem(
book: BookShelfItem, book: BookShelfItem,
layoutMode: Int, layoutMode: Int,
modifier: Modifier = Modifier,
gridStyle: Int = 0, gridStyle: Int = 0,
isCompact: Boolean = false, isCompact: Boolean = false,
isUpdating: Boolean = false, isUpdating: Boolean = false,
@@ -403,6 +404,7 @@ fun BookItem(
isGrid = layoutMode != 0, isGrid = layoutMode != 0,
gridStyle = gridStyle, gridStyle = gridStyle,
isCompact = isCompact, isCompact = isCompact,
modifier = modifier,
cover = { modifier -> cover = { modifier ->
BookshelfCover( BookshelfCover(
name = book.name, 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.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding 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.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -197,20 +199,18 @@ fun BookshelfScreen(
} }
val bookGroupStyle = BookshelfConfig.bookGroupStyle val bookGroupStyle = BookshelfConfig.bookGroupStyle
// 控制是å�¦å¤„于“文件夹列表â€�根视图,还是“文件夹内部â€�书ç±�视å›? // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) } var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
val baseTitle = when { val baseTitle = when (bookGroupStyle) {
bookGroupStyle == 1 -> { 1 -> {
uiState.groups.getOrNull(pagerState.currentPage)?.groupName uiState.groups.getOrNull(pagerState.currentPage)?.groupName
?: stringResource(R.string.bookshelf) ?: stringResource(R.string.bookshelf)
} }
2 if uiState.groups.isNotEmpty() -> {
bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> {
if (isInFolderRoot) stringResource(R.string.bookshelf) if (isInFolderRoot) stringResource(R.string.bookshelf)
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName else uiState.groups.getOrNull(pagerState.currentPage)?.groupName
?: stringResource(R.string.bookshelf) ?: stringResource(R.string.bookshelf)
} }
else -> stringResource(R.string.bookshelf) else -> stringResource(R.string.bookshelf)
} }
val title = if (uiState.upBooksCount > 0) { val title = if (uiState.upBooksCount > 0) {
@@ -329,7 +329,7 @@ fun BookshelfScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.adaptiveHorizontalPadding(), .adaptiveHorizontalPaddingTab(),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
val tabTitles = remember(uiState.groups) { val tabTitles = remember(uiState.groups) {
@@ -600,6 +600,19 @@ fun BookshelfPage(
onBookClick: (BookShelfItem) -> Unit, onBookClick: (BookShelfItem) -> Unit,
onBookLongClick: (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 columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
val isGridMode = bookshelfLayoutMode != 0 val isGridMode = bookshelfLayoutMode != 0
val totalHorizontalPadding = val totalHorizontalPadding =
@@ -623,6 +636,7 @@ fun BookshelfPage(
items(books, key = { it.bookUrl }) { book -> items(books, key = { it.bookUrl }) { book ->
BookItem( BookItem(
book = book, book = book,
modifier = Modifier.animateItem(),
layoutMode = bookshelfLayoutMode, layoutMode = bookshelfLayoutMode,
gridStyle = BookshelfConfig.bookshelfGridLayout, gridStyle = BookshelfConfig.bookshelfGridLayout,
isCompact = BookshelfConfig.bookshelfLayoutCompact, isCompact = BookshelfConfig.bookshelfLayoutCompact,
@@ -27,6 +27,8 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.book.group.GroupEditContent 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.book.group.GroupViewModel
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.SmallIconButton
@@ -48,6 +50,7 @@ fun GroupManageSheet(
val groups by bookshelfViewModel.allGroupsFlow.collectAsState() val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
var editingGroup by remember { mutableStateOf<BookGroup?>(null) } var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
var isEditing by remember { mutableStateOf(false) } var isEditing by remember { mutableStateOf(false) }
var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) }
var listData by remember { mutableStateOf(groups) } var listData by remember { mutableStateOf(groups) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -76,15 +79,36 @@ fun GroupManageSheet(
show = show, show = show,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = if (!isEditing) stringResource(R.string.group_manage) else stringResource(R.string.group_edit), 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 = { endAction = {
if (!isEditing) { if (!isEditing) {
SmallIconButton( SmallIconButton(
onClick = { onClick = {
editingGroup = null editingGroup = null
coverPath = null
isEditing = true isEditing = true
}, },
imageVector = Icons.Default.Add imageVector = Icons.Default.Add
) )
} else {
GroupResetCoverAction(
group = editingGroup,
onCoverPathChange = { coverPath = it },
viewModel = viewModel
)
} }
} }
) { ) {
@@ -98,7 +122,12 @@ fun GroupManageSheet(
if (editing) { if (editing) {
GroupEditContent( GroupEditContent(
group = editingGroup, group = editingGroup,
onDismissRequest = { isEditing = false }, onDismissRequest = {
editingGroup = null
isEditing = false
},
coverPath = coverPath,
onCoverPathChange = { coverPath = it },
viewModel = viewModel viewModel = viewModel
) )
} else { } else {
@@ -115,12 +144,13 @@ fun GroupManageSheet(
title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() }, title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null, subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
isEnabled = group.show, isEnabled = group.show,
containerColor = LegadoTheme.colorScheme.surface, containerColor = LegadoTheme.colorScheme.onSheetContent,
onEnabledChange = { isChecked -> onEnabledChange = { isChecked ->
viewModel.upGroup(group.copy(show = isChecked)) viewModel.upGroup(group.copy(show = isChecked))
}, },
onClickEdit = { onClickEdit = {
editingGroup = group editingGroup = group
coverPath = group.cover
isEditing = true 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.divider.PillHeaderDivider
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows 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.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.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.list.ListScaffold import io.legado.app.ui.widget.components.list.ListScaffold
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
@@ -159,6 +160,19 @@ fun ExploreScreen(
} }
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) { 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( FastScrollLazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), 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.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.AlertButton 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.ToggleChip
import io.legado.app.ui.widget.components.button.TopBarActionButton import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton import io.legado.app.ui.widget.components.button.TopBarNavigationButton
@@ -368,21 +369,20 @@ fun GroupSelector(
} }
} }
} }
IconButton(onClick = onManageClick) { MediumIconButton(
Icon(Icons.Default.Settings, "Manage") onClick = onManageClick,
} imageVector = Icons.Default.Settings
)
} }
} }
@Composable @Composable
fun ManageGroupDialog( fun ManageGroupDialog(
show: Boolean, // 💡 新增 show 参数,外层直接传值,不要用 if 包裹! show: Boolean,
groups: List<String>, groups: List<String>,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onDelete: (List<String>) -> Unit onDelete: (List<String>) -> Unit
) { ) {
// 💡 使用 Set 来管理多选状态,并且以 show 作为 key。
// 这样每次重新打开弹窗时,选中状态会自动清空,防止上次的残留
var selectedGroups by remember(show) { mutableStateOf(emptySet<String>()) } var selectedGroups by remember(show) { mutableStateOf(emptySet<String>()) }
AppAlertDialog( AppAlertDialog(
@@ -395,7 +395,6 @@ fun ManageGroupDialog(
} else { } else {
Column( Column(
modifier = Modifier.verticalScroll(rememberScrollState()), modifier = Modifier.verticalScroll(rememberScrollState()),
// 去掉了 spacedBy(8.dp),因为 CheckboxItem 内部已经有了合适的 padding
) { ) {
groups.forEach { group -> groups.forEach { group ->
val isSelected = selectedGroups.contains(group) val isSelected = selectedGroups.contains(group)
@@ -418,7 +417,6 @@ fun ManageGroupDialog(
confirmText = "删除选中", confirmText = "删除选中",
onConfirm = { onConfirm = {
onDelete(selectedGroups.toList()) onDelete(selectedGroups.toList())
// 注意:外层需要在 onDelete 回调中将 show 置为 false
}, },
dismissText = "关闭", dismissText = "关闭",
onDismiss = onDismiss onDismiss = onDismiss
@@ -34,7 +34,7 @@ import io.legado.app.R
import io.legado.app.data.entities.RssStar import io.legado.app.data.entities.RssStar
import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.rss.read.ReadRssActivity
import io.legado.app.ui.widget.components.ActionItem 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.SourceIcon
import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -186,7 +186,7 @@ fun RssFavoritesScreen(
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
EmptyMessageView( EmptyMessage(
message = "还没有收藏订阅!", message = "还没有收藏订阅!",
isLoading = state.isLoading isLoading = state.isLoading
) )
@@ -203,7 +203,7 @@ fun RssFavoritesScreen(
SelectionItemCard( SelectionItemCard(
title = rssStar.title, title = rssStar.title,
subtitle = if (rssStar.group.isNotBlank()) { subtitle = if (rssStar.group.isNotBlank()) {
"${rssStar.group} �${rssStar.pubDate ?: ""}" "${rssStar.group} �?${rssStar.pubDate ?: ""}"
} else { } else {
rssStar.pubDate 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.automirrored.filled.Sort
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import io.legado.app.ui.widget.components.AppFloatingActionButton import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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.association.ImportRssSourceDialog
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField 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.alert.AppAlertDialog
import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
@@ -123,7 +120,7 @@ fun RuleSubScreen(
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
EmptyMessageView( EmptyMessage(
message = stringResource(R.string.rule_sub_empty_msg), message = stringResource(R.string.rule_sub_empty_msg),
isLoading = state.isLoading isLoading = state.isLoading
) )
@@ -14,6 +14,13 @@ fun Modifier.adaptiveHorizontalPadding(): Modifier {
return this.padding(horizontal = horizontal) 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 @Composable
fun Modifier.adaptiveHorizontalPadding( fun Modifier.adaptiveHorizontalPadding(
vertical: Dp, vertical: Dp,
@@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import io.legado.app.ui.config.themeConfig.ThemeConfig 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.MiuixTheme
import top.yukonga.miuix.kmp.theme.ThemeController import top.yukonga.miuix.kmp.theme.ThemeController
@@ -194,8 +196,10 @@ fun AppTheme(
onTertiaryFixed = miuixColorScheme.onTertiaryContainer, onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer, onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
cardContainer = miuixColorScheme.disabledPrimary, cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f)
onCardContainer = miuixColorScheme.primary .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 * 仅判断 enableBlur 的简单 HazeEffect
*/ */
@@ -14,14 +14,15 @@ import androidx.core.graphics.scale
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.request.SuccessResult import coil.request.SuccessResult
import coil.size.Size
import com.materialkolor.quantize.QuantizerCelebi import com.materialkolor.quantize.QuantizerCelebi
import com.materialkolor.score.Score import com.materialkolor.score.Score
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 256 private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 128
private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 128 private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 64
private const val IMAGE_MAX_QUANTIZE_COLORS = 128 private const val IMAGE_MAX_QUANTIZE_COLORS = 64
private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt() private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt()
suspend fun ImageLoader.extractSeedColor( suspend fun ImageLoader.extractSeedColor(
@@ -32,6 +33,7 @@ suspend fun ImageLoader.extractSeedColor(
val request = ImageRequest.Builder(context) val request = ImageRequest.Builder(context)
.data(data) .data(data)
.allowHardware(false) .allowHardware(false)
.size(Size(IMAGE_COLOR_EXTRACT_SIZE_PX, IMAGE_COLOR_EXTRACT_SIZE_PX))
.apply(configureRequest) .apply(configureRequest)
.build() .build()
@@ -73,7 +73,8 @@ data class LegadoColorScheme(
val onTertiaryFixedVariant: Color, val onTertiaryFixedVariant: Color,
val cardContainer: Color, val cardContainer: Color,
val onCardContainer: Color val onCardContainer: Color,
val onSheetContent: Color
) )
data class LegadoTypography( data class LegadoTypography(
@@ -68,7 +68,8 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
onTertiaryFixed = onTertiaryFixed, onTertiaryFixed = onTertiaryFixed,
onTertiaryFixedVariant = onTertiaryFixedVariant, onTertiaryFixedVariant = onTertiaryFixedVariant,
cardContainer = primaryContainer.copy(alpha = 0.5f), cardContainer = primaryContainer.copy(alpha = 0.5f),
onCardContainer = primary onCardContainer = primary,
onSheetContent = surface
) )
} }
@@ -55,6 +55,7 @@ fun ProvideThemeOverride(
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
var appliedTheme by remember { mutableStateOf<ThemeOverrideState?>(null) } var appliedTheme by remember { mutableStateOf<ThemeOverrideState?>(null) }
val baseTheme = LocalLegadoThemeColors.current
LaunchedEffect(theme) { LaunchedEffect(theme) {
if (theme == null) { if (theme == null) {
@@ -66,15 +67,16 @@ fun ProvideThemeOverride(
} }
val currentTheme = appliedTheme val currentTheme = appliedTheme
if (currentTheme == null) { ?: ThemeOverrideState(
content() seedColor = baseTheme.seedColor,
} else { colorScheme = baseTheme.colorScheme
)
ProvideColorSchemeOverride( ProvideColorSchemeOverride(
colorScheme = currentTheme.colorScheme, colorScheme = currentTheme.colorScheme,
seedColor = currentTheme.seedColor, seedColor = currentTheme.seedColor,
content = content content = content
) )
}
} }
@Composable @Composable
@@ -49,3 +49,33 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium) 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 { 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 @Composable
@ReadOnlyComposable @ReadOnlyComposable
fun ultraThin( 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.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.theme.MiuixTheme 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 import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -300,13 +301,48 @@ fun AppDenseTextField(
scrollState: ScrollState = rememberScrollState(), scrollState: ScrollState = rememberScrollState(),
shape: Shape = TextFieldDefaults.shape, shape: Shape = TextFieldDefaults.shape,
interactionSource: MutableInteractionSource? = null, 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( AppTextField(
state = state, state = state,
modifier = modifier.heightIn(min = 48.dp), modifier = modifier.heightIn(min = denseMinHeight),
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
backgroundColor = backgroundColor, backgroundColor = denseBackgroundColor,
label = label, label = label,
labelPosition = labelPosition, labelPosition = labelPosition,
placeholder = placeholder, placeholder = placeholder,
@@ -28,7 +28,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun EmptyMessageView( fun EmptyMessage(
message: String, message: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isLoading: Boolean = false, isLoading: Boolean = false,
@@ -81,7 +81,7 @@ fun EmptyMessageView(
} }
@Composable @Composable
fun EmptyMessageView( fun EmptyMessage(
@StringRes messageResId: Int, @StringRes messageResId: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isLoading: Boolean = false, isLoading: Boolean = false,
@@ -93,7 +93,7 @@ fun EmptyMessageView(
onFaceClick: (() -> Unit)? = null onFaceClick: (() -> Unit)? = null
) { ) {
val message = stringResource(id = messageResId) val message = stringResource(id = messageResId)
EmptyMessageView( EmptyMessage(
message = message, message = message,
modifier = modifier, modifier = modifier,
isLoading = isLoading, isLoading = isLoading,
@@ -9,14 +9,9 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -49,7 +44,6 @@ fun SearchBarSection(
trailingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null,
dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null
) { ) {
var showMenu by remember { mutableStateOf(false) }
val textFieldState = rememberTextFieldState(initialText = query) val textFieldState = rememberTextFieldState(initialText = query)
LaunchedEffect(query) { LaunchedEffect(query) {
@@ -76,30 +70,20 @@ fun SearchBarSection(
if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow
} }
val modifier = modifier if (isMiuix) {
.fillMaxWidth()
.then(
if (isMiuix) modifier.padding(vertical = 4.dp)
else modifier
)
val searchTextField = @Composable {
AppDenseTextField( AppDenseTextField(
state = textFieldState, state = textFieldState,
modifier = modifier, modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
placeholder = { AppText(placeholder) }, placeholder = { AppText(placeholder) },
leadingIcon = leadingIcon, leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
lineLimits = TextFieldLineLimits.SingleLine, lineLimits = TextFieldLineLimits.SingleLine,
backgroundColor = if (!isMiuix) { backgroundColor = resolvedBackgroundColor,
Color.Transparent miuixUseSearchBarInputField = true,
} else { miuixSearchBarLabel = placeholder
resolvedBackgroundColor
}
) )
}
if (isMiuix) {
searchTextField()
} else { } else {
Surface( Surface(
modifier = Modifier modifier = Modifier
@@ -108,7 +92,15 @@ fun SearchBarSection(
shape = RoundedCornerShape(32.dp), shape = RoundedCornerShape(32.dp),
color = resolvedBackgroundColor 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?, data: T?,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
title: String? = null, title: String? = null,
text: String? = null,
confirmText: String = "确定", confirmText: String = "确定",
onConfirm: ((T) -> Unit)? = null, onConfirm: ((T) -> Unit)? = null,
dismissText: String = "取消", dismissText: String = "取消",
@@ -173,6 +174,7 @@ fun <T> AppAlertDialog(
show = data != null, show = data != null,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = title, title = title,
text = text,
modifier = modifier, modifier = modifier,
confirmText = confirmText, confirmText = confirmText,
onConfirm = onConfirm?.let { { it(currentData) } }, onConfirm = onConfirm?.let { { it(currentData) } },
@@ -73,7 +73,8 @@ fun SmallIconButton(
) { ) {
MiuixIcon( MiuixIcon(
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize),
) )
} }
} else { } else {
@@ -1,13 +1,19 @@
package io.legado.app.ui.widget.components.button 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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp 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.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText 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.Button as MiuixButton
import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults 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 @Composable
fun SmallTextButton( fun SmallTextButton(
@@ -27,14 +35,31 @@ fun SmallTextButton(
onClick: () -> Unit onClick: () -> Unit
) { ) {
if (ThemeResolver.isMiuixEngine(composeEngine)) { if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixButton( Card(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
colors = MiuixButtonDefaults.buttonColors() showIndication = true,
) { colors = CardDefaults.defaultColors(
MiuixText( color = LegadoTheme.colorScheme.surfaceVariant,
text = text 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 { } else {
TextButton( TextButton(
@@ -46,14 +46,15 @@ fun ToggleChip(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp) modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)
) { ) {
AnimatedVisibility(visible = selected) { AnimatedVisibility(visible = selected) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
MiuixIcon( MiuixIcon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = checkedContentDescription, 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)) Spacer(Modifier.width(6.dp))
} }
@@ -61,9 +62,10 @@ fun ToggleChip(
MiuixText( MiuixText(
text = label, text = label,
style = LegadoTheme.typography.labelSmall, style = LegadoTheme.typography.labelMediumEmphasized,
maxLines = 1, 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.BorderStroke
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -39,6 +40,7 @@ private fun BaseCard(
color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)), color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)),
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
) )
if (onClick != null) {
MiuixCard( MiuixCard(
modifier = modifier, modifier = modifier,
cornerRadius = cornerRadius, cornerRadius = cornerRadius,
@@ -48,6 +50,14 @@ private fun BaseCard(
content = content, content = content,
colors = colors colors = colors
) )
} else {
MiuixCard(
modifier = modifier,
cornerRadius = cornerRadius,
content = content,
colors = colors
)
}
} else { } else {
val colors = CardDefaults.cardColors( val colors = CardDefaults.cardColors(
containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy( containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy(
@@ -60,7 +70,7 @@ private fun BaseCard(
if (onClick != null) { if (onClick != null) {
Card( Card(
modifier = modifier, modifier = modifier,
shape = shape, shape = RoundedCornerShape(cornerRadius),
colors = colors, colors = colors,
elevation = elevation, elevation = elevation,
border = border, border = border,
@@ -70,7 +80,7 @@ private fun BaseCard(
} else { } else {
Card( Card(
modifier = modifier, modifier = modifier,
shape = shape, shape = RoundedCornerShape(cornerRadius),
colors = colors, colors = colors,
elevation = elevation, elevation = elevation,
border = border, 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.theme.ThemeResolver
import io.legado.app.ui.widget.components.AdaptiveSwitch import io.legado.app.ui.widget.components.AdaptiveSwitch
import io.legado.app.ui.widget.components.button.SmallIconButton 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.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.ReorderableItem
@@ -70,8 +72,6 @@ fun SelectionItemCard(
containerColor: Color? = null, containerColor: Color? = null,
selectedContainerColor: Color? = null selectedContainerColor: Color? = null
) { ) {
var showMenu by remember { mutableStateOf(false) }
val composeEngine = ThemeResolver.isMiuixEngine(composeEngine) val composeEngine = ThemeResolver.isMiuixEngine(composeEngine)
val animatedContainerColor by animateColorAsState( val animatedContainerColor by animateColorAsState(
targetValue = if (isSelected) targetValue = if (isSelected)
@@ -92,6 +92,38 @@ fun SelectionItemCard(
containerColor = animatedContainerColor, containerColor = animatedContainerColor,
elevation = CardDefaults.cardElevation(defaultElevation = elevation) elevation = CardDefaults.cardElevation(defaultElevation = elevation)
) { ) {
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( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -110,7 +142,7 @@ fun SelectionItemCard(
label = "LeadingContent" label = "LeadingContent"
) { selectionMode -> ) { selectionMode ->
if (selectionMode) { if (selectionMode) {
Checkbox( AppCheckbox(
checked = isSelected, checked = isSelected,
onCheckedChange = null onCheckedChange = null
) )
@@ -121,7 +153,7 @@ fun SelectionItemCard(
} }
} }
if (composeEngine) { if (ThemeResolver.isMiuixEngine(composeEngine)) {
BasicComponent( BasicComponent(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
@@ -189,7 +221,7 @@ fun SelectionItemCard(
if (onClickEdit != null) { if (onClickEdit != null) {
SmallIconButton( SmallIconButton(
onClick = onClickEdit, onClick = onClickEdit,
imageVector = Icons.Default.Edit, imageVector = AppIcons.Edit,
contentDescription = "Edit" contentDescription = "Edit"
) )
} }
@@ -202,7 +234,7 @@ fun SelectionItemCard(
Box { Box {
SmallIconButton( SmallIconButton(
onClick = { showMenu = true }, onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert, imageVector = AppIcons.MoreVert,
contentDescription = "More" contentDescription = "More"
) )
RoundDropdownMenu( RoundDropdownMenu(
@@ -215,7 +247,6 @@ fun SelectionItemCard(
} }
} }
} }
}
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -44,8 +44,7 @@ fun TextCard(
NormalCard( NormalCard(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(cornerRadius), cornerRadius = cornerRadius,
cornerRadius = 4.dp,
containerColor = finalBackgroundColor, containerColor = finalBackgroundColor,
contentColor = finalContentColor, contentColor = finalContentColor,
onClick = onClick onClick = onClick
@@ -14,27 +14,26 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@Composable @Composable
fun CheckboxItem( fun CheckboxItem(
title: String, title: String,
color: Color = Color.Transparent, color: Color = LegadoTheme.colorScheme.onSheetContent,
checked: Boolean, checked: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit
) { ) {
val alpha = if (enabled) 1f else 0.5f val alpha = if (enabled) 1f else 0.5f
GlassCard(
cornerRadius = 12.dp,
containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer
else color,
shape = MaterialTheme.shapes.small
)
.clickable(enabled = enabled) { onCheckedChange(!checked) } .clickable(enabled = enabled) { onCheckedChange(!checked) }
.padding(vertical = 12.dp, horizontal = 12.dp), .padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -54,4 +53,6 @@ fun CheckboxItem(
.alpha(alpha) .alpha(alpha)
) )
} }
}
} }
@@ -39,9 +39,10 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.withSave import androidx.core.graphics.withSave
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.ui.config.coverConfig.CoverConfig import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.theme.LegadoTheme 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 org.koin.compose.koinInject
import io.legado.app.model.BookCover as BookCoverModel import io.legado.app.model.BookCover as BookCoverModel
@@ -81,6 +82,9 @@ fun BookCover(
isFinalPathLoaded = false isFinalPathLoaded = false
} }
NormalCard(
cornerRadius = 4.dp
){
BoxWithConstraints( BoxWithConstraints(
modifier = modifier modifier = modifier
.aspectRatio(5f / 7f) .aspectRatio(5f / 7f)
@@ -89,7 +93,6 @@ fun BookCover(
Modifier.shadow(4.dp, RoundedCornerShape(4.dp)) Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
} else Modifier } else Modifier
) )
.clip(RoundedCornerShape(4.dp))
.then( .then(
if (!hasCustomDefault && !isOnlineCoverLoaded) { if (!hasCustomDefault && !isOnlineCoverLoaded) {
Modifier.background(LegadoTheme.colorScheme.surfaceContainer) Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
@@ -103,10 +106,13 @@ fun BookCover(
if (hasCustomDefault && !isFinalPathLoaded) { if (hasCustomDefault && !isFinalPathLoaded) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = buildCoverImageRequest(
.data(randomPath) context = context,
.crossfade(true) data = randomPath,
.build(), sourceOrigin = null,
loadOnlyWifi = false,
crossfade = true,
),
contentDescription = null, contentDescription = null,
imageLoader = koinInject(), imageLoader = koinInject(),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
@@ -116,12 +122,13 @@ fun BookCover(
if (finalPath != null) { if (finalPath != null) {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = buildCoverImageRequest(
.data(finalPath) context = context,
.crossfade(true) data = finalPath,
.setParameter("sourceOrigin", sourceOrigin) sourceOrigin = sourceOrigin,
.setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi) loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
.build(), crossfade = true,
),
contentDescription = null, contentDescription = null,
imageLoader = koinInject(), imageLoader = koinInject(),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
@@ -162,6 +169,7 @@ fun BookCover(
} }
} }
}
} }
@Composable @Composable
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
@Composable @Composable
@@ -42,6 +43,7 @@ fun BookshelfCover(
TextCard( TextCard(
text = badgeText, text = badgeText,
icon = if (showBadgeDot) Icons.Default.Update else null, icon = if (showBadgeDot) Icons.Default.Update else null,
iconSize = 12.dp,
backgroundColor = LegadoTheme.colorScheme.cardContainer, backgroundColor = LegadoTheme.colorScheme.cardContainer,
contentColor = LegadoTheme.colorScheme.onCardContainer, contentColor = LegadoTheme.colorScheme.onCardContainer,
modifier = Modifier modifier = Modifier
@@ -49,7 +51,7 @@ fun BookshelfCover(
.padding(2.dp), .padding(2.dp),
cornerRadius = 4.dp, cornerRadius = 4.dp,
horizontalPadding = 4.dp, horizontalPadding = 4.dp,
verticalPadding = 0.dp verticalPadding = 2.dp
) )
} }
@@ -63,20 +65,17 @@ fun BookshelfCover(
.padding(2.dp), .padding(2.dp),
cornerRadius = 4.dp, cornerRadius = 4.dp,
horizontalPadding = 4.dp, horizontalPadding = 4.dp,
verticalPadding = 0.dp verticalPadding = 2.dp
) )
} }
if (isUpdating) { if (isUpdating) {
LinearProgressIndicator( AppLinearProgressIndicator(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp, vertical = 6.dp) .padding(horizontal = 4.dp, vertical = 6.dp)
.height(3.dp), .height(3.dp)
color = LegadoTheme.colorScheme.primary,
trackColor = LegadoTheme.colorScheme.primary.copy(alpha = 0.2f),
strokeCap = StrokeCap.Round
) )
} }
} }
@@ -24,7 +24,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.ImageLoader import coil.ImageLoader
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -57,16 +56,18 @@ fun Cover(
) )
} else { } else {
AsyncImage( AsyncImage(
model = ImageRequest.Builder(context) model = buildCoverImageRequest(
.data(path) context = context,
.crossfade(true) data = path,
.setParameter("sourceOrigin", sourceOrigin) sourceOrigin = sourceOrigin,
.setParameter("loadOnlyWifi", loadOnlyWifi) loadOnlyWifi = loadOnlyWifi,
.listener( crossfade = true,
) {
listener(
onSuccess = { _, _ -> onLoadFinish?.invoke() }, onSuccess = { _, _ -> onLoadFinish?.invoke() },
onError = { _, _ -> onLoadFinish?.invoke() } onError = { _, _ -> onLoadFinish?.invoke() }
) )
.build(), },
imageLoader = imageLoader, imageLoader = imageLoader,
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
@@ -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.filled.LibraryBooks
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
import androidx.compose.material.icons.filled.Clear 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.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.Person
import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.RssFeed import androidx.compose.material.icons.filled.RssFeed
import androidx.compose.material.icons.filled.Search 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.Explore
import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.Person
import androidx.compose.material.icons.outlined.RssFeed 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.main.MainDestination
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import okhttp3.internal.http2.Settings
import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Search import top.yukonga.miuix.kmp.icon.basic.Search
import top.yukonga.miuix.kmp.icon.extended.Album import top.yukonga.miuix.kmp.icon.extended.Album
import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.icon.extended.Close import top.yukonga.miuix.kmp.icon.extended.Close
import top.yukonga.miuix.kmp.icon.extended.ContactsBook 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.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.Refresh
import top.yukonga.miuix.kmp.icon.extended.Settings 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 { object AppIcons {
@@ -38,6 +54,18 @@ object AppIcons {
@Composable @Composable
get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search 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 val Close: ImageVector
@Composable @Composable
get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear
@@ -46,6 +74,26 @@ object AppIcons {
@Composable @Composable
get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack 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 val Replay: ImageVector
@Composable @Composable
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
@@ -45,7 +45,7 @@ fun RoundDropdownMenu(
if (isMiuix) { if (isMiuix) {
val popupContentColor = LegadoTheme.colorScheme.onSurface val popupContentColor = LegadoTheme.colorScheme.onSurface
OverlayListPopup( WindowListPopup(
show = expanded, show = expanded,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
popupModifier = modifier popupModifier = modifier
@@ -137,7 +137,7 @@ fun RoundDropdownMenuItem(
else -> defaultContentColor.copy(alpha = 0.38f) else -> defaultContentColor.copy(alpha = 0.38f)
} }
} }
val containerColor = if (isSelected) colorScheme.primaryContainer else colorScheme.surface val containerColor = colorScheme.surface
Surface( Surface(
onClick = onClick, onClick = onClick,
@@ -76,7 +76,7 @@ fun <T> RuleListScaffold(
show = showDeleteConfirmDialog, show = showDeleteConfirmDialog,
onDismissRequest = { showDeleteConfirmDialog = false }, onDismissRequest = { showDeleteConfirmDialog = false },
title = stringResource(R.string.delete), title = stringResource(R.string.delete),
text = stringResource(R.string.del_msg), text = stringResource(R.string.sure_del),
confirmText = stringResource(R.string.ok), confirmText = stringResource(R.string.ok),
onConfirm = { onConfirm = {
onDeleteSelected(state.selectedIds) onDeleteSelected(state.selectedIds)
@@ -2,6 +2,7 @@
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row 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.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R 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.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
@@ -41,7 +43,6 @@ import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
@Composable @Composable
fun SliderSettingItem( fun SliderSettingItem(
title: String, title: String,
color: Color? = null,
value: Float, value: Float,
defaultValue: Float, defaultValue: Float,
valueRange: ClosedFloatingPointRange<Float>, valueRange: ClosedFloatingPointRange<Float>,
@@ -56,7 +57,10 @@ fun SliderSettingItem(
if (ThemeResolver.isMiuixEngine(composeEngine)) { if (ThemeResolver.isMiuixEngine(composeEngine)) {
Column(modifier = Modifier.fillMaxWidth()) { Column(
modifier = Modifier
.fillMaxWidth()
) {
BasicComponent( BasicComponent(
title = title, title = title,
summary = description, summary = description,
@@ -143,7 +147,7 @@ fun SliderSettingItem(
} else { } else {
SettingItem( SettingItem(
title = title, title = title,
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow, color = MaterialTheme.colorScheme.surfaceContainerLow,
option = description, option = description,
expanded = expanded, expanded = expanded,
onExpandChange = { expanded = it }, onExpandChange = { expanded = it },
@@ -75,7 +75,7 @@ fun <T> DynamicTopAppBar(
if (!isSelecting) { if (!isSelecting) {
TopBarActionButton( TopBarActionButton(
onClick = { onSearchToggle(!state.isSearch) }, onClick = { onSearchToggle(!state.isSearch) },
imageVector = Icons.Default.Search, imageVector = AppIcons.Search,
contentDescription = "搜索" contentDescription = "搜索"
) )
+1 -1
View File
@@ -17,7 +17,7 @@ kotlinxSerialization = "1.10.0"
kotlinxCoroutinesAndroid = "1.10.2" kotlinxCoroutinesAndroid = "1.10.2"
kotlinxSerializationJson = "1.10.0" kotlinxSerializationJson = "1.10.0"
ksp = "2.3.6" ksp = "2.3.6"
agp = "9.1.0" agp = "9.1.1"
appcompat = "1.7.1" appcompat = "1.7.1"
colorpicker = "1.1.0" colorpicker = "1.1.0"
colorpicker-compose = "1.1.3" colorpicker-compose = "1.1.3"