[优化] 优化界面
This commit is contained in:
60 files changed
+1908
-1832
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,23 +158,29 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
CompactDropdownSettingItem(
|
GlassCard(
|
||||||
title = stringResource(R.string.sort),
|
containerColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
selectedValue = selectedSortIndex.toString(),
|
) {
|
||||||
displayEntries = sortOptions,
|
CompactDropdownSettingItem(
|
||||||
entryValues = sortEntryValues,
|
title = stringResource(R.string.sort),
|
||||||
onValueChange = {
|
selectedValue = selectedSortIndex.toString(),
|
||||||
selectedSortIndex = it.toInt()
|
color = LegadoTheme.colorScheme.onSheetContent,
|
||||||
}
|
displayEntries = sortOptions,
|
||||||
)
|
entryValues = sortEntryValues,
|
||||||
|
onValueChange = {
|
||||||
|
selectedSortIndex = it.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,79 +194,91 @@ 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 (groupName.isEmpty()) {
|
||||||
if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
|
appCtx.toastOnUi("分组名称不能为空")
|
||||||
OutlinedIconButton(onClick = {
|
} else {
|
||||||
context.alert(R.string.delete, R.string.sure_del) {
|
if (group != null && group.groupId != 0b1L) {
|
||||||
yesButton {
|
viewModel.upGroup(
|
||||||
viewModel.delGroup(group) {
|
group.copy(
|
||||||
onDismissRequest()
|
groupName = groupName,
|
||||||
}
|
cover = coverPath,
|
||||||
}
|
bookSort = selectedSortIndex,
|
||||||
noButton()
|
enableRefresh = enableRefresh
|
||||||
}
|
)
|
||||||
}) {
|
) {
|
||||||
Icon(
|
onDismissRequest()
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = stringResource(R.string.delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedIconButton(onClick = {
|
|
||||||
if (group != null) {
|
|
||||||
viewModel.clearCover(group) {
|
|
||||||
coverPath = null
|
|
||||||
appCtx.toastOnUi("封面已重置")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
coverPath = null
|
viewModel.addGroup(
|
||||||
}
|
groupName,
|
||||||
}) {
|
selectedSortIndex,
|
||||||
Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset))
|
enableRefresh,
|
||||||
}
|
coverPath
|
||||||
}
|
) {
|
||||||
|
onDismissRequest()
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
OutlinedButton(onClick = onDismissRequest) {
|
|
||||||
AppText(stringResource(R.string.cancel))
|
|
||||||
}
|
|
||||||
Button(onClick = {
|
|
||||||
if (groupName.isEmpty()) {
|
|
||||||
appCtx.toastOnUi("分组名称不能为空")
|
|
||||||
} else {
|
|
||||||
if (group != null) {
|
|
||||||
viewModel.upGroup(
|
|
||||||
group.copy(
|
|
||||||
groupName = groupName,
|
|
||||||
cover = coverPath,
|
|
||||||
bookSort = selectedSortIndex,
|
|
||||||
enableRefresh = enableRefresh
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
viewModel.addGroup(
|
|
||||||
groupName,
|
|
||||||
selectedSortIndex,
|
|
||||||
enableRefresh,
|
|
||||||
coverPath
|
|
||||||
) {
|
|
||||||
onDismissRequest()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) {
|
|
||||||
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,33 +526,23 @@ 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(
|
val newServer = server?.copy(name = name) ?: Server(name = name)
|
||||||
onClick = onCancel,
|
val newConfig = JSONObject().apply {
|
||||||
modifier = Modifier.weight(1f)
|
put("url", url)
|
||||||
) {
|
put("username", username)
|
||||||
AppText(stringResource(android.R.string.cancel))
|
put("password", password)
|
||||||
}
|
}
|
||||||
Button(
|
newServer.config = newConfig.toString()
|
||||||
onClick = {
|
onSave(newServer)
|
||||||
val newServer = server?.copy(name = name) ?: Server(name = name)
|
},
|
||||||
val newConfig = JSONObject().apply {
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
put("url", url)
|
confirmText = "保存",
|
||||||
put("username", username)
|
confirmEnabled = name.isNotBlank() && url.isNotBlank()
|
||||||
put("password", password)
|
)
|
||||||
}
|
|
||||||
newServer.config = newConfig.toString()
|
|
||||||
onSave(newServer)
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
enabled = 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,63 +185,16 @@ 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(
|
state = state,
|
||||||
title = "",
|
showMenu = showMenu,
|
||||||
color = appBarColor,
|
onShowMenuChange = { showMenu = it },
|
||||||
navigationIcon = {
|
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
||||||
TopBarNavigationButton(
|
onBackPressed = { onIntent(BookInfoIntent.BackPressed) },
|
||||||
onClick = { onIntent(BookInfoIntent.BackPressed) }
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
BookInfoTopBarActions(
|
|
||||||
state = state,
|
|
||||||
showMenu = showMenu,
|
|
||||||
onShowMenuChange = { showMenu = it },
|
|
||||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
scrollBehavior = miuixScrollBehavior?.miuixBehavior
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = "",
|
|
||||||
maxLines = 1,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
TopBarNavigationButton(
|
|
||||||
onClick = { onIntent(BookInfoIntent.BackPressed) }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
BookInfoTopBarActions(
|
|
||||||
state = state,
|
|
||||||
showMenu = showMenu,
|
|
||||||
onShowMenuChange = { showMenu = it },
|
|
||||||
onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = appBarColor,
|
|
||||||
scrolledContainerColor = appBarColor,
|
|
||||||
navigationIconContentColor = LegadoTheme.colorScheme.onSurface,
|
|
||||||
titleContentColor = LegadoTheme.colorScheme.onSurface,
|
|
||||||
actionIconContentColor = LegadoTheme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
@@ -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) }
|
||||||
)
|
)
|
||||||
RoundDropdownMenuItem(
|
if (book?.isLocal == false ){
|
||||||
text = stringResource(R.string.allow_update),
|
RoundDropdownMenuItem(
|
||||||
onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
|
text = stringResource(R.string.allow_update),
|
||||||
trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) }
|
onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) },
|
||||||
)
|
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,53 +136,58 @@ 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()) {
|
||||||
items(groups, key = { it.groupId }) { group ->
|
LazyColumn(
|
||||||
val isSelected = selectedGroupId and group.groupId > 0
|
modifier = Modifier.heightIn(max = 600.dp),
|
||||||
SelectionItemCard(
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
title = group.groupName,
|
) {
|
||||||
isSelected = isSelected,
|
items(groups, key = { it.groupId }) { group ->
|
||||||
onToggleSelection = {
|
val isSelected = selectedGroupId and group.groupId > 0
|
||||||
selectedGroupId = if (isSelected) {
|
SelectionItemCard(
|
||||||
selectedGroupId - group.groupId
|
title = group.groupName,
|
||||||
} else {
|
isSelected = isSelected,
|
||||||
selectedGroupId + group.groupId
|
onToggleSelection = {
|
||||||
}
|
selectedGroupId = if (isSelected) {
|
||||||
},
|
selectedGroupId - group.groupId
|
||||||
leadingContent = {
|
} else {
|
||||||
AppCheckbox(
|
selectedGroupId + group.groupId
|
||||||
checked = isSelected,
|
|
||||||
onCheckedChange = {
|
|
||||||
selectedGroupId = if (it) {
|
|
||||||
selectedGroupId + group.groupId
|
|
||||||
} else {
|
|
||||||
selectedGroupId - group.groupId
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
},
|
leadingContent = {
|
||||||
trailingAction = {
|
AppCheckbox(
|
||||||
SmallIconButton(
|
checked = isSelected,
|
||||||
onClick = { editingGroup = group },
|
onCheckedChange = {
|
||||||
imageVector = Icons.Default.Edit
|
selectedGroupId = if (it) {
|
||||||
)
|
selectedGroupId + group.groupId
|
||||||
},
|
} else {
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
selectedGroupId - group.groupId
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = { editingGroup = group },
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
ConfirmDismissButtonsRow(
|
||||||
|
onDismiss = onDismissRequest,
|
||||||
|
onConfirm = { onConfirm(selectedGroupId) },
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
ConfirmDismissButtonsRow(
|
|
||||||
onDismiss = onDismissRequest,
|
|
||||||
onConfirm = { onConfirm(selectedGroupId) },
|
|
||||||
dismissText = stringResource(R.string.cancel),
|
|
||||||
confirmText = stringResource(R.string.ok),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null })
|
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
|
startActivity<BookInfoActivity> {
|
||||||
binding.searchView.text.toString().trim().let {
|
putExtra("name", name)
|
||||||
binding.searchView.setText(it)
|
putExtra("author", author)
|
||||||
|
putExtra("bookUrl", bookUrl)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
onOpenSourceManage = {
|
||||||
|
startActivity<BookSourceActivity>()
|
||||||
}
|
}
|
||||||
|
)
|
||||||
R.id.menu_search_scope -> alertSearchScope()
|
|
||||||
R.id.menu_source_manage -> startActivity<BookSourceActivity>()
|
|
||||||
R.id.menu_log -> showDialogFragment(AppLogDialog())
|
|
||||||
R.id.menu_1 -> {
|
|
||||||
viewModel.searchScope.update("")
|
|
||||||
updateSelectedGroup()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
if (item.groupId == R.id.menu_group_1) {
|
|
||||||
viewModel.searchScope.remove(item.title.toString())
|
|
||||||
} else if (item.groupId == R.id.menu_group_2) {
|
|
||||||
viewModel.searchScope.update(item.title.toString())
|
|
||||||
}
|
|
||||||
updateSelectedGroup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onCompatOptionsItemSelected(item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMaterialSearch() {
|
private fun dispatchInit(intent: Intent?) {
|
||||||
binding.chipPrecisionSearch.isChecked = getPrefBoolean(PreferKey.precisionSearch)
|
viewModel.onIntent(
|
||||||
binding.chipPrecisionSearch.setOnCheckedChangeListener { chip, isChecked ->
|
SearchIntent.Initialize(
|
||||||
putPrefBoolean(PreferKey.precisionSearch, isChecked)
|
key = intent?.getStringExtra("key"),
|
||||||
precisionSearchMenuItem?.isChecked = isChecked
|
scopeRaw = intent?.getStringExtra("searchScope"),
|
||||||
binding.searchView.text.toString().trim().let {
|
)
|
||||||
binding.searchView.setText(it)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.chipSearchScope.setOnClickListener {
|
|
||||||
alertSearchScope()
|
|
||||||
}
|
|
||||||
binding.chipGroup.setOnClickListener { view ->
|
|
||||||
binding.chipGroup.isChecked = true
|
|
||||||
val popup = PopupMenu(this, view)
|
|
||||||
val menu = popup.menu
|
|
||||||
buildSearchScopeMenu(menu)
|
|
||||||
updateSearchScopeChip()
|
|
||||||
var fromGroup1 = false
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
|
||||||
when (item.groupId) {
|
|
||||||
R.id.menu_group_1 -> {
|
|
||||||
viewModel.searchScope.remove(item.title.toString())
|
|
||||||
fromGroup1 = true
|
|
||||||
}
|
|
||||||
R.id.menu_group_2 -> {
|
|
||||||
if (item.itemId == R.id.menu_1) {
|
|
||||||
viewModel.searchScope.update("")
|
|
||||||
fromGroup1 = true
|
|
||||||
} else {
|
|
||||||
viewModel.searchScope.update(item.title.toString())
|
|
||||||
fromGroup1 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
viewModel.searchScope.update(item.title.toString())
|
|
||||||
fromGroup1 = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSelectedGroup()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
popup.setOnDismissListener {
|
|
||||||
binding.chipGroup.isChecked = !fromGroup1
|
|
||||||
updateSearchScopeChip()
|
|
||||||
}
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBar.setOnMenuItemClickListener { item ->
|
|
||||||
onCompatOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBar.setNavigationOnClickListener {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.setupWithSearchBar(searchBar)
|
|
||||||
|
|
||||||
binding.searchView.show()
|
|
||||||
binding.searchView.editText.requestFocus()
|
|
||||||
|
|
||||||
searchView.editText.hint = getString(R.string.search_book_key)
|
|
||||||
searchView.editText.setOnEditorActionListener { _, actionId, _ ->
|
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
|
|
||||||
doSearch()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.editText.setOnKeyListener { _, keyCode, event ->
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) {
|
|
||||||
doSearch()
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.editText.doAfterTextChanged {
|
|
||||||
viewModel.stop()
|
|
||||||
binding.fbStartStop.invisible()
|
|
||||||
upHistory(it?.toString()?.trim().orEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.addTransitionListener { _, _, newState ->
|
|
||||||
if (newState == SearchView.TransitionState.HIDDEN) {
|
|
||||||
searchBar.setText(searchView.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchView.addTransitionListener { _, oldState, newState ->
|
|
||||||
if (oldState == SearchView.TransitionState.HIDDEN && newState == SearchView.TransitionState.SHOWN) {
|
|
||||||
searchView.editText.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
visibleInputHelp()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun doSearch() {
|
|
||||||
val key = searchView.text.toString().trim()
|
|
||||||
if (key.isNotEmpty()) {
|
|
||||||
isManualStopSearch = false
|
|
||||||
viewModel.saveSearchKey(key)
|
|
||||||
viewModel.searchKey = ""
|
|
||||||
viewModel.search(key)
|
|
||||||
visibleInputHelp()
|
|
||||||
searchView.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSelectedGroup() {
|
|
||||||
val displayNames = viewModel.searchScope.displayNames
|
|
||||||
val groupName = displayNames.firstOrNull().orEmpty()
|
|
||||||
binding.chipGroup.text = groupName.ifEmpty { getString(R.string.search_select_group) }
|
|
||||||
binding.chipGroup.isChecked = groupName.isNotEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildSearchScopeMenu(menu: Menu) {
|
|
||||||
menu.transaction {
|
|
||||||
menu.removeGroup(R.id.menu_group_1)
|
|
||||||
menu.removeGroup(R.id.menu_group_2)
|
|
||||||
var hasChecked = false
|
|
||||||
val searchScopeNames = viewModel.searchScope.displayNames
|
|
||||||
|
|
||||||
if (viewModel.searchScope.isSource()) {
|
|
||||||
menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, searchScopeNames.first()).apply {
|
|
||||||
isChecked = true
|
|
||||||
hasChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val allSourceMenu =
|
|
||||||
menu.add(R.id.menu_group_2, R.id.menu_1, Menu.NONE, getString(R.string.all_source))
|
|
||||||
.apply {
|
|
||||||
if (searchScopeNames.isEmpty()) {
|
|
||||||
isChecked = true
|
|
||||||
hasChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
groups?.forEach { groupName ->
|
|
||||||
if (searchScopeNames.contains(groupName)) {
|
|
||||||
menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, groupName).apply {
|
|
||||||
isChecked = true
|
|
||||||
hasChecked = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
menu.add(R.id.menu_group_2, Menu.NONE, Menu.NONE, groupName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasChecked) {
|
|
||||||
viewModel.searchScope.update("")
|
|
||||||
allSourceMenu.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.setGroupCheckable(R.id.menu_group_1, true, false)
|
|
||||||
menu.setGroupCheckable(R.id.menu_group_2, true, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
|
||||||
binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this)
|
|
||||||
binding.rvBookshelfSearch.adapter = bookAdapter
|
|
||||||
binding.rvBookshelfSearch.itemAnimator = DefaultItemAnimator()
|
|
||||||
|
|
||||||
binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this)
|
|
||||||
binding.rvHistoryKey.adapter = historyKeyAdapter
|
|
||||||
binding.rvHistoryKey.itemAnimator = DefaultItemAnimator()
|
|
||||||
|
|
||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
binding.recyclerView.itemAnimator = DefaultItemAnimator()
|
|
||||||
|
|
||||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeInserted(positionStart, itemCount)
|
|
||||||
if (positionStart == 0) {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
|
||||||
super.onItemRangeMoved(fromPosition, toPosition, itemCount)
|
|
||||||
if (toPosition == 0) {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
|
||||||
if (!recyclerView.canScrollVertically(1)) {
|
|
||||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
|
||||||
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
|
|
||||||
if (lastPosition == RecyclerView.NO_POSITION) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val lastView = layoutManager.findViewByPosition(lastPosition)
|
|
||||||
if (lastView == null) {
|
|
||||||
scrollToBottom()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val bottom =
|
|
||||||
abs(lastView.bottom - recyclerView.height) - recyclerView.paddingBottom
|
|
||||||
if (bottom <= 1) {
|
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOtherView() {
|
|
||||||
binding.llStop.applyNavigationBarMargin()
|
|
||||||
binding.fbStartStop.setOnClickListener {
|
|
||||||
if (viewModel.isSearchLiveData.value == true) {
|
|
||||||
isManualStopSearch = true
|
|
||||||
viewModel.stop()
|
|
||||||
} else {
|
|
||||||
viewModel.search("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvClearHistory.setOnClickListener { alertClearHistory() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() {
|
|
||||||
viewModel.searchScope.stateLiveData.observe(this) {
|
|
||||||
if (!binding.llInputHelp.isVisible) {
|
|
||||||
searchView.text.toString().trim().let {
|
|
||||||
searchView.setText(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.isSearchLiveData.observe(this) {
|
|
||||||
if (it) {
|
|
||||||
startSearch()
|
|
||||||
} else {
|
|
||||||
searchFinally()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.searchBookLiveData.observe(this) {
|
|
||||||
adapter.setItems(it)
|
|
||||||
if (it.isNullOrEmpty()) {
|
|
||||||
binding.tvEmptyMsg.visible()
|
|
||||||
} else {
|
|
||||||
binding.tvEmptyMsg.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
appDb.bookSourceDao.flowEnabledGroups().collect {
|
|
||||||
groups = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
viewModel.resume()
|
|
||||||
try {
|
|
||||||
awaitCancellation()
|
|
||||||
} finally {
|
|
||||||
viewModel.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理传入数据
|
|
||||||
*/
|
|
||||||
private fun receiptIntent(intent: Intent? = null) {
|
|
||||||
val searchScope = intent?.getStringExtra("searchScope")
|
|
||||||
searchScope?.let {
|
|
||||||
viewModel.searchScope.update(searchScope, false)
|
|
||||||
updateSearchScopeChip()
|
|
||||||
}
|
|
||||||
val key = intent?.getStringExtra("key")
|
|
||||||
if (key.isNullOrBlank()) {
|
|
||||||
searchView.editText.requestFocus()
|
|
||||||
} else {
|
|
||||||
searchView.setText(key)
|
|
||||||
searchView.postDelayed({doSearch()},500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 滚动到底部事件
|
|
||||||
*/
|
|
||||||
private fun scrollToBottom() {
|
|
||||||
if (isManualStopSearch) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (viewModel.isSearchLiveData.value == false
|
|
||||||
&& viewModel.searchKey.isNotEmpty()
|
|
||||||
&& viewModel.hasMore
|
|
||||||
) {
|
|
||||||
viewModel.search("")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开关闭输入帮助
|
|
||||||
*/
|
|
||||||
private fun visibleInputHelp() {
|
|
||||||
upHistory(searchView.text.toString())
|
|
||||||
binding.llInputHelp.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新搜索历史
|
|
||||||
*/
|
|
||||||
private fun upHistory(key: String? = null) {
|
|
||||||
booksFlowJob?.cancel()
|
|
||||||
booksFlowJob = lifecycleScope.launch {
|
|
||||||
if (key.isNullOrBlank()) {
|
|
||||||
binding.tvBookShow.gone()
|
|
||||||
binding.rvBookshelfSearch.gone()
|
|
||||||
} else {
|
|
||||||
appDb.bookDao.flowSearch(key).conflate().collect {
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
binding.tvBookShow.gone()
|
|
||||||
binding.rvBookshelfSearch.gone()
|
|
||||||
} else {
|
|
||||||
binding.tvBookShow.visible()
|
|
||||||
binding.rvBookshelfSearch.visible()
|
|
||||||
}
|
|
||||||
bookAdapter.setItems(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
historyFlowJob?.cancel()
|
|
||||||
historyFlowJob = lifecycleScope.launch {
|
|
||||||
when {
|
|
||||||
key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime()
|
|
||||||
else -> appDb.searchKeywordDao.flowSearch(key)
|
|
||||||
}.catch {
|
|
||||||
AppLog.put("搜索界面获取搜索历史数据失败\n${it.localizedMessage}", it)
|
|
||||||
}.flowOn(IO).conflate().collect {
|
|
||||||
historyKeyAdapter.setItems(it)
|
|
||||||
if (it.isEmpty()) {
|
|
||||||
binding.tvClearHistory.invisible()
|
|
||||||
} else {
|
|
||||||
binding.tvClearHistory.visible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 开始搜索
|
|
||||||
*/
|
|
||||||
private fun startSearch() {
|
|
||||||
binding.refreshProgressBar.visible()
|
|
||||||
binding.cdProgress.visible()
|
|
||||||
//binding.refreshProgressBar.isAutoLoading = true
|
|
||||||
binding.fbStartStop.setImageResource(R.drawable.ic_stop_black_24dp)
|
|
||||||
binding.fbStartStop.visible()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索结束
|
|
||||||
*/
|
|
||||||
private fun searchFinally() {
|
|
||||||
TransitionManager.beginDelayedTransition(binding.root)
|
|
||||||
binding.refreshProgressBar.gone()
|
|
||||||
binding.cdProgress.gone()
|
|
||||||
if (!isManualStopSearch && viewModel.hasMore) {
|
|
||||||
binding.fbStartStop.setImageResource(R.drawable.ic_play)
|
|
||||||
} else {
|
|
||||||
binding.fbStartStop.invisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
viewModel.upAdapterLiveData.observe(this) {
|
|
||||||
adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null))
|
|
||||||
}
|
|
||||||
viewModel.searchProgressLiveData.observe(this) { (processed, total) ->
|
|
||||||
val progress = (processed * 100 / total).coerceAtMost(100)
|
|
||||||
binding.refreshProgressBar.setProgress(progress, true)
|
|
||||||
binding.tvProgress.text = "$processed / $total"
|
|
||||||
}
|
|
||||||
viewModel.searchFinishLiveData.observe(this) { isEmpty ->
|
|
||||||
if (!isEmpty || viewModel.searchScope.isAll()) return@observe
|
|
||||||
alert("搜索结果为空") {
|
|
||||||
val precisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch)
|
|
||||||
val displayScope = viewModel.searchScope.display
|
|
||||||
if (precisionSearch) {
|
|
||||||
setMessage("${displayScope}分组搜索结果为空,是否关闭精准搜索?")
|
|
||||||
yesButton {
|
|
||||||
appCtx.putPrefBoolean(PreferKey.precisionSearch, false)
|
|
||||||
precisionSearchMenuItem?.isChecked = false
|
|
||||||
viewModel.searchKey = ""
|
|
||||||
viewModel.search(searchView.text.toString())
|
|
||||||
doSearch()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setMessage("${displayScope}分组搜索结果为空,是否切换到全部分组?")
|
|
||||||
yesButton {
|
|
||||||
viewModel.searchScope.update("")
|
|
||||||
doSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
noButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示书籍详情
|
|
||||||
*/
|
|
||||||
override fun showBookInfo(name: String, author: String, bookUrl: String) {
|
|
||||||
startActivity<BookInfoActivity> {
|
|
||||||
putExtra("name", name)
|
|
||||||
putExtra("author", author)
|
|
||||||
putExtra("bookUrl", bookUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否已经加入书架
|
|
||||||
*/
|
|
||||||
override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState {
|
|
||||||
return viewModel.isInBookShelf(name, author, url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 显示书籍详情
|
|
||||||
*/
|
|
||||||
override fun showBookInfo(book: Book) {
|
|
||||||
showBookInfo(book.name, book.author, book.bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点击历史关键字
|
|
||||||
*/
|
|
||||||
override fun searchHistory(key: String) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
when {
|
|
||||||
searchView.text.toString() == key -> {
|
|
||||||
searchView.setText(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> {
|
|
||||||
searchView.setText(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
searchView.setText(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除搜索记录
|
|
||||||
*/
|
|
||||||
override fun deleteHistory(searchKeyword: SearchKeyword) {
|
|
||||||
viewModel.deleteHistory(searchKeyword)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onSearchScopeOk(searchScope: SearchScope) {
|
|
||||||
viewModel.searchScope.update(searchScope.toString())
|
|
||||||
updateSearchScopeChip()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSearchScopeChip() {
|
|
||||||
binding.chipSearchScope.isCheckable = true
|
|
||||||
val scopeNames = viewModel.searchScope.displayNames
|
|
||||||
if (scopeNames.isEmpty() || viewModel.searchScope.isAll()) {
|
|
||||||
binding.chipSearchScope.apply {
|
|
||||||
isChecked = false
|
|
||||||
text = getString(R.string.all_source)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.chipSearchScope.apply {
|
|
||||||
isChecked = true
|
|
||||||
text = scopeNames.joinToString(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.chipSearchScope.isCheckable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun alertSearchScope() {
|
|
||||||
showDialogFragment<SearchScopeDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun alertClearHistory() {
|
|
||||||
alert(R.string.draw) {
|
|
||||||
setMessage(R.string.sure_clear_search_history)
|
|
||||||
yesButton {
|
|
||||||
viewModel.clearHistory()
|
|
||||||
}
|
|
||||||
noButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun finish() {
|
|
||||||
if (searchView.hasFocus()) {
|
|
||||||
searchView.clearFocus()
|
|
||||||
}
|
|
||||||
super.finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
|
||||||
|
fun onIntent(intent: SearchIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is SearchIntent.Initialize -> initialize(intent.key, intent.scopeRaw)
|
||||||
|
is SearchIntent.UpdateQuery -> updateQuery(intent.query, showSuggestions = true)
|
||||||
|
SearchIntent.SubmitSearch -> submitSearch()
|
||||||
|
SearchIntent.LoadMore -> loadMore()
|
||||||
|
SearchIntent.StopSearch -> stopSearch()
|
||||||
|
SearchIntent.PauseEngine -> searchSession.pause()
|
||||||
|
SearchIntent.ResumeEngine -> searchSession.resume()
|
||||||
|
is SearchIntent.UseHistoryKeyword -> {
|
||||||
|
updateQuery(intent.keyword, showSuggestions = false)
|
||||||
|
submitSearch(intent.keyword)
|
||||||
}
|
}
|
||||||
}.onError {
|
|
||||||
AppLog.put("加载书架数据失败", it)
|
is SearchIntent.OpenSearchBook -> {
|
||||||
|
emitEffect(
|
||||||
|
SearchEffect.OpenBookInfo(
|
||||||
|
name = intent.book.name,
|
||||||
|
author = intent.book.author,
|
||||||
|
bookUrl = intent.book.bookUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.OpenBookshelfBook -> {
|
||||||
|
emitEffect(
|
||||||
|
SearchEffect.OpenBookInfo(
|
||||||
|
name = intent.book.name,
|
||||||
|
author = intent.book.author,
|
||||||
|
bookUrl = intent.book.bookUrl,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.DeleteHistory -> viewModelScope.launch {
|
||||||
|
repository.deleteSearchKeyword(intent.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.SetClearHistoryDialogVisible -> {
|
||||||
|
_uiState.update { it.copy(showClearHistoryDialog = intent.visible) }
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIntent.ConfirmClearHistory -> {
|
||||||
|
_uiState.update { it.copy(showClearHistoryDialog = false) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.clearSearchKeywords()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.SetScopeSheetVisible -> {
|
||||||
|
_uiState.update { it.copy(showScopeSheet = intent.visible) }
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIntent.SelectAllScope -> {
|
||||||
|
searchScope.update("")
|
||||||
|
syncScopeState()
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
|
||||||
|
is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
|
||||||
|
is SearchIntent.RemoveScopeItem -> {
|
||||||
|
searchScope.remove(intent.scopeName)
|
||||||
|
syncScopeState()
|
||||||
|
}
|
||||||
|
|
||||||
|
is SearchIntent.TogglePrecision -> {
|
||||||
|
appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled)
|
||||||
|
_uiState.update { it.copy(isPrecisionSearch = intent.enabled) }
|
||||||
|
restartCommittedSearchIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIntent.ConfirmEmptyScopeAction -> handleEmptyScopeActionConfirmed()
|
||||||
|
SearchIntent.DismissEmptyScopeAction -> {
|
||||||
|
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInBookShelf(name: String, author: String, url: String?): BookShelfState {
|
fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
|
||||||
val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url }
|
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,116 +38,102 @@ 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)
|
|
||||||
)
|
|
||||||
|
|
||||||
SliderSettingItem(
|
GlassCard {
|
||||||
title = stringResource(R.string.check_source_timeout),
|
SliderSettingItem(
|
||||||
color = MaterialTheme.colorScheme.surface,
|
title = stringResource(R.string.check_source_timeout),
|
||||||
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(
|
) {
|
||||||
title = stringResource(R.string.search),
|
CheckboxItem(
|
||||||
checked = viewModel.checkSearch,
|
title = stringResource(R.string.search),
|
||||||
onCheckedChange = {
|
checked = viewModel.checkSearch,
|
||||||
viewModel.checkSearch = it
|
onCheckedChange = {
|
||||||
if (!it && !viewModel.checkDiscovery) {
|
viewModel.checkSearch = it
|
||||||
viewModel.checkDiscovery = true
|
if (!it && !viewModel.checkDiscovery) {
|
||||||
}
|
viewModel.checkDiscovery = true
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
item {
|
CheckboxItem(
|
||||||
CheckboxItem(
|
title = stringResource(R.string.discovery),
|
||||||
title = stringResource(R.string.discovery),
|
checked = viewModel.checkDiscovery,
|
||||||
checked = viewModel.checkDiscovery,
|
onCheckedChange = {
|
||||||
onCheckedChange = {
|
viewModel.checkDiscovery = it
|
||||||
viewModel.checkDiscovery = it
|
if (!it && !viewModel.checkSearch) {
|
||||||
if (!it && !viewModel.checkSearch) {
|
viewModel.checkSearch = true
|
||||||
viewModel.checkSearch = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CheckboxItem(
|
||||||
|
title = stringResource(R.string.source_tab_info),
|
||||||
|
checked = viewModel.checkInfo,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.checkInfo = it
|
||||||
|
if (!it) {
|
||||||
|
viewModel.checkCategory = false
|
||||||
|
viewModel.checkContent = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CheckboxItem(
|
||||||
|
title = stringResource(R.string.chapter_list),
|
||||||
|
checked = viewModel.checkCategory,
|
||||||
|
enabled = viewModel.checkInfo,
|
||||||
|
onCheckedChange = {
|
||||||
|
viewModel.checkCategory = it
|
||||||
|
if (!it) viewModel.checkContent = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CheckboxItem(
|
||||||
|
title = stringResource(R.string.source_tab_content),
|
||||||
|
checked = viewModel.checkContent,
|
||||||
|
enabled = viewModel.checkCategory,
|
||||||
|
onCheckedChange = { viewModel.checkContent = it }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmDismissButtonsRow(
|
||||||
CheckboxGroupContainer(columns = 3) {
|
|
||||||
item {
|
|
||||||
CheckboxItem(
|
|
||||||
title = stringResource(R.string.source_tab_info),
|
|
||||||
checked = viewModel.checkInfo,
|
|
||||||
onCheckedChange = {
|
|
||||||
viewModel.checkInfo = it
|
|
||||||
if (!it) {
|
|
||||||
viewModel.checkCategory = false
|
|
||||||
viewModel.checkContent = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CheckboxItem(
|
|
||||||
title = stringResource(R.string.chapter_list),
|
|
||||||
checked = viewModel.checkCategory,
|
|
||||||
enabled = viewModel.checkInfo,
|
|
||||||
onCheckedChange = {
|
|
||||||
viewModel.checkCategory = it
|
|
||||||
if (!it) viewModel.checkContent = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
CheckboxItem(
|
|
||||||
title = stringResource(R.string.source_tab_content),
|
|
||||||
checked = viewModel.checkContent,
|
|
||||||
enabled = viewModel.checkCategory,
|
|
||||||
onCheckedChange = { viewModel.checkContent = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
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)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+71
-60
@@ -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,57 +69,68 @@ fun DirectLinkUploadBottomSheet(
|
|||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
show = show,
|
show = show,
|
||||||
title = stringResource(R.string.direct_link_upload_config),
|
title = stringResource(R.string.direct_link_upload_config),
|
||||||
endAction = {
|
startAction = {
|
||||||
MediumIconButton(
|
MediumIconButton(
|
||||||
onClick = { showMenu = true },
|
onClick = {
|
||||||
imageVector = Icons.Default.MoreVert
|
viewModel.testRule { result -> showTestResult = result }
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Checklist
|
||||||
)
|
)
|
||||||
RoundDropdownMenu(
|
},
|
||||||
expanded = showMenu,
|
endAction = {
|
||||||
onDismissRequest = { showMenu = false }
|
Box {
|
||||||
) {
|
MediumIconButton(
|
||||||
RoundDropdownMenuItem(
|
onClick = { showMenu = true },
|
||||||
text = "导入默认",
|
imageVector = Icons.Default.MoreVert
|
||||||
leadingIcon = { Icon(Icons.Default.Download, null) },
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
|
|
||||||
viewModel.upView(rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenu(
|
||||||
text = stringResource(R.string.copy_rule),
|
expanded = showMenu,
|
||||||
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
|
onDismissRequest = { showMenu = false }
|
||||||
onClick = {
|
) {
|
||||||
showMenu = false
|
RoundDropdownMenuItem(
|
||||||
val rule = DirectLinkUpload.Rule(
|
text = "导入默认",
|
||||||
viewModel.uploadUrl,
|
leadingIcon = { Icon(Icons.Default.Download, null) },
|
||||||
viewModel.downloadUrlRule,
|
onClick = {
|
||||||
viewModel.summary,
|
showMenu = false
|
||||||
viewModel.compress
|
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
|
||||||
)
|
|
||||||
context.sendToClip(GSON.toJson(rule))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
text = stringResource(R.string.paste_rule),
|
|
||||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
|
||||||
onClick = {
|
|
||||||
showMenu = false
|
|
||||||
runCatching {
|
|
||||||
context.getClipText()?.let {
|
|
||||||
val rule =
|
|
||||||
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
|
|
||||||
.getOrThrow()
|
|
||||||
viewModel.upView(rule)
|
viewModel.upView(rule)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
|
||||||
context.toastOnUi("剪贴板为空或格式不对")
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
RoundDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.copy_rule),
|
||||||
|
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
val rule = DirectLinkUpload.Rule(
|
||||||
|
viewModel.uploadUrl,
|
||||||
|
viewModel.downloadUrlRule,
|
||||||
|
viewModel.summary,
|
||||||
|
viewModel.compress
|
||||||
|
)
|
||||||
|
context.sendToClip(GSON.toJson(rule))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.paste_rule),
|
||||||
|
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
runCatching {
|
||||||
|
context.getClipText()?.let {
|
||||||
|
val rule =
|
||||||
|
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
|
||||||
|
.getOrThrow()
|
||||||
|
viewModel.upView(rule)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.toastOnUi("剪贴板为空或格式不对")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
onDismissRequest = onDismiss
|
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 = {
|
||||||
) {
|
prevKeys = ""
|
||||||
OutlinedButton(
|
nextKeys = ""
|
||||||
onClick = {
|
},
|
||||||
prevKeys = ""
|
onConfirm = {
|
||||||
nextKeys = ""
|
ReadConfig.prevKeys = prevKeys
|
||||||
},
|
ReadConfig.nextKeys = nextKeys
|
||||||
modifier = Modifier.Companion.weight(1f)
|
onDismissRequest()
|
||||||
) {
|
},
|
||||||
AppText(stringResource(R.string.reset))
|
dismissText = stringResource(R.string.reset),
|
||||||
}
|
confirmText = stringResource(R.string.ok)
|
||||||
|
)
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
ReadConfig.prevKeys = prevKeys
|
|
||||||
ReadConfig.nextKeys = nextKeys
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
modifier = Modifier.Companion.weight(1f)
|
|
||||||
) {
|
|
||||||
AppText(stringResource(R.string.ok))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+11
-23
@@ -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,24 +117,14 @@ 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
|
||||||
colorScheme = currentTheme.colorScheme,
|
|
||||||
seedColor = currentTheme.seedColor,
|
|
||||||
content = content
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
ProvideColorSchemeOverride(
|
||||||
|
colorScheme = currentTheme.colorScheme,
|
||||||
|
seedColor = currentTheme.seedColor,
|
||||||
|
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,
|
||||||
|
|||||||
+3
-3
@@ -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,15 +40,24 @@ 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
|
||||||
)
|
)
|
||||||
MiuixCard(
|
if (onClick != null) {
|
||||||
modifier = modifier,
|
MiuixCard(
|
||||||
cornerRadius = cornerRadius,
|
modifier = modifier,
|
||||||
pressFeedbackType = pressFeedbackType,
|
cornerRadius = cornerRadius,
|
||||||
showIndication = true,
|
pressFeedbackType = pressFeedbackType,
|
||||||
onClick = onClick,
|
showIndication = true,
|
||||||
content = content,
|
onClick = onClick,
|
||||||
colors = colors
|
content = content,
|
||||||
)
|
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,48 +92,104 @@ fun SelectionItemCard(
|
|||||||
containerColor = animatedContainerColor,
|
containerColor = animatedContainerColor,
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
|
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
|
||||||
) {
|
) {
|
||||||
Row(
|
SelectionItemCardContent(
|
||||||
modifier = Modifier
|
title = title,
|
||||||
.fillMaxWidth()
|
subtitle = subtitle,
|
||||||
.animateContentSize(),
|
supportingContent = supportingContent,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
isEnabled = isEnabled,
|
||||||
|
isSelected = isSelected,
|
||||||
|
inSelectionMode = inSelectionMode,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
onEnabledChange = onEnabledChange,
|
||||||
|
onClickEdit = onClickEdit,
|
||||||
|
trailingAction = trailingAction,
|
||||||
|
dropdownContent = dropdownContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectionItemCardContent(
|
||||||
|
title: String,
|
||||||
|
subtitle: String? = null,
|
||||||
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
inSelectionMode: Boolean = false,
|
||||||
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
|
onEnabledChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onClickEdit: (() -> Unit)? = null,
|
||||||
|
trailingAction: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateContentSize(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = inSelectionMode || leadingContent != null
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
Box(
|
||||||
visible = inSelectionMode || leadingContent != null
|
modifier = Modifier.padding(start = 12.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Box(
|
AnimatedContent(
|
||||||
modifier = Modifier.padding(start = 12.dp),
|
targetState = inSelectionMode,
|
||||||
contentAlignment = Alignment.Center
|
label = "LeadingContent"
|
||||||
) {
|
) { selectionMode ->
|
||||||
AnimatedContent(
|
if (selectionMode) {
|
||||||
targetState = inSelectionMode,
|
AppCheckbox(
|
||||||
label = "LeadingContent"
|
checked = isSelected,
|
||||||
) { selectionMode ->
|
onCheckedChange = null
|
||||||
if (selectionMode) {
|
)
|
||||||
Checkbox(
|
} else {
|
||||||
checked = isSelected,
|
leadingContent?.invoke()
|
||||||
onCheckedChange = null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
leadingContent?.invoke()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (composeEngine) {
|
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||||
BasicComponent(
|
BasicComponent(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
style = LegadoTheme.typography.titleSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
when {
|
||||||
|
supportingContent != null -> supportingContent()
|
||||||
|
!subtitle.isNullOrBlank() -> {
|
||||||
|
AppText(
|
||||||
|
text = subtitle,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
headlineContent = {
|
||||||
AppText(
|
AppText(
|
||||||
text = title,
|
text = title,
|
||||||
style = LegadoTheme.typography.titleSmall,
|
style = LegadoTheme.typography.titleSmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
when {
|
},
|
||||||
supportingContent != null -> supportingContent()
|
supportingContent = when {
|
||||||
!subtitle.isNullOrBlank() -> {
|
supportingContent != null -> supportingContent
|
||||||
|
!subtitle.isNullOrBlank() -> {
|
||||||
|
{
|
||||||
AppText(
|
AppText(
|
||||||
text = subtitle,
|
text = subtitle,
|
||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.bodySmall,
|
||||||
@@ -142,75 +198,50 @@ fun SelectionItemCard(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
else -> null
|
||||||
} else {
|
},
|
||||||
ListItem(
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
||||||
modifier = Modifier.weight(1f),
|
)
|
||||||
headlineContent = {
|
}
|
||||||
AppText(
|
|
||||||
text = title,
|
Row(
|
||||||
style = LegadoTheme.typography.titleSmall,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
maxLines = 1,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
overflow = TextOverflow.Ellipsis
|
modifier = Modifier
|
||||||
)
|
.padding(end = 8.dp)
|
||||||
},
|
) {
|
||||||
supportingContent = when {
|
onEnabledChange?.let {
|
||||||
supportingContent != null -> supportingContent
|
AdaptiveSwitch(
|
||||||
!subtitle.isNullOrBlank() -> {
|
modifier = Modifier.scale(0.8f),
|
||||||
{
|
checked = isEnabled,
|
||||||
AppText(
|
onCheckedChange = it
|
||||||
text = subtitle,
|
|
||||||
style = LegadoTheme.typography.bodySmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
if (onClickEdit != null) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
SmallIconButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
onClick = onClickEdit,
|
||||||
modifier = Modifier
|
imageVector = AppIcons.Edit,
|
||||||
.padding(end = 8.dp)
|
contentDescription = "Edit"
|
||||||
) {
|
)
|
||||||
onEnabledChange?.let {
|
}
|
||||||
AdaptiveSwitch(
|
|
||||||
modifier = Modifier.scale(0.8f),
|
|
||||||
checked = isEnabled,
|
|
||||||
onCheckedChange = it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onClickEdit != null) {
|
if (trailingAction != null) {
|
||||||
|
trailingAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropdownContent != null) {
|
||||||
|
Box {
|
||||||
SmallIconButton(
|
SmallIconButton(
|
||||||
onClick = onClickEdit,
|
onClick = { showMenu = true },
|
||||||
imageVector = Icons.Default.Edit,
|
imageVector = AppIcons.MoreVert,
|
||||||
contentDescription = "Edit"
|
contentDescription = "More"
|
||||||
)
|
)
|
||||||
}
|
RoundDropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
if (trailingAction != null) {
|
onDismissRequest = { showMenu = false }
|
||||||
trailingAction()
|
) {
|
||||||
}
|
dropdownContent { showMenu = false }
|
||||||
|
|
||||||
if (dropdownContent != null) {
|
|
||||||
Box {
|
|
||||||
SmallIconButton(
|
|
||||||
onClick = { showMenu = true },
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "More"
|
|
||||||
)
|
|
||||||
RoundDropdownMenu(
|
|
||||||
expanded = showMenu,
|
|
||||||
onDismissRequest = { showMenu = false }
|
|
||||||
) {
|
|
||||||
dropdownContent { showMenu = false }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,44 +14,45 @@ 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
|
||||||
|
|
||||||
Row(
|
GlassCard(
|
||||||
modifier = Modifier
|
cornerRadius = 12.dp,
|
||||||
.fillMaxWidth()
|
containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color,
|
||||||
.clip(MaterialTheme.shapes.small)
|
|
||||||
.background(
|
|
||||||
color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer
|
|
||||||
else color,
|
|
||||||
shape = MaterialTheme.shapes.small
|
|
||||||
)
|
|
||||||
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
|
||||||
.padding(vertical = 12.dp, horizontal = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
AppCheckbox(
|
Row(
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = null,
|
|
||||||
enabled = enabled,
|
|
||||||
modifier = Modifier.alpha(alpha)
|
|
||||||
)
|
|
||||||
AppText(
|
|
||||||
text = title,
|
|
||||||
style = LegadoTheme.typography.bodyMediumEmphasized,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 12.dp)
|
.fillMaxWidth()
|
||||||
.alpha(alpha)
|
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
||||||
)
|
.padding(vertical = 12.dp, horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppCheckbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = null,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = Modifier.alpha(alpha)
|
||||||
|
)
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
style = LegadoTheme.typography.bodyMediumEmphasized,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
.alpha(alpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -39,9 +39,10 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.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,86 +82,93 @@ fun BookCover(
|
|||||||
isFinalPathLoaded = false
|
isFinalPathLoaded = false
|
||||||
}
|
}
|
||||||
|
|
||||||
BoxWithConstraints(
|
NormalCard(
|
||||||
modifier = modifier
|
cornerRadius = 4.dp
|
||||||
.aspectRatio(5f / 7f)
|
){
|
||||||
.then(
|
BoxWithConstraints(
|
||||||
if (CoverConfig.coverShowShadow) {
|
modifier = modifier
|
||||||
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
.aspectRatio(5f / 7f)
|
||||||
} else Modifier
|
.then(
|
||||||
)
|
if (CoverConfig.coverShowShadow) {
|
||||||
.clip(RoundedCornerShape(4.dp))
|
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||||
.then(
|
} else Modifier
|
||||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
)
|
||||||
Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
|
.then(
|
||||||
} else Modifier
|
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||||
)
|
Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
} else Modifier
|
||||||
val density = LocalDensity.current
|
)
|
||||||
val iconSizeDp = with(density) {
|
) {
|
||||||
(minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
|
val density = LocalDensity.current
|
||||||
}
|
val iconSizeDp = with(density) {
|
||||||
|
(minOf(maxWidth, maxHeight).toPx() / 3f).toDp()
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
contentDescription = null,
|
loadOnlyWifi = false,
|
||||||
imageLoader = koinInject(),
|
crossfade = true,
|
||||||
contentScale = ContentScale.Crop,
|
),
|
||||||
modifier = Modifier.fillMaxSize()
|
contentDescription = null,
|
||||||
)
|
imageLoader = koinInject(),
|
||||||
}
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
),
|
||||||
imageLoader = koinInject(),
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
imageLoader = koinInject(),
|
||||||
modifier = Modifier.fillMaxSize(),
|
contentScale = ContentScale.Crop,
|
||||||
onSuccess = {
|
modifier = Modifier.fillMaxSize(),
|
||||||
isOnlineCoverLoaded = true
|
onSuccess = {
|
||||||
isFinalPathLoaded = true
|
isOnlineCoverLoaded = true
|
||||||
onLoadFinish?.invoke()
|
isFinalPathLoaded = true
|
||||||
},
|
onLoadFinish?.invoke()
|
||||||
onError = {
|
},
|
||||||
isOnlineCoverLoaded = false
|
onError = {
|
||||||
|
isOnlineCoverLoaded = false
|
||||||
|
onLoadFinish?.invoke()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
onLoadFinish?.invoke()
|
onLoadFinish?.invoke()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
} else {
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
onLoadFinish?.invoke()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
if (!hasCustomDefault && !isOnlineCoverLoaded) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Book,
|
Icons.Default.Book,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = LegadoTheme.colorScheme.secondary,
|
tint = LegadoTheme.colorScheme.secondary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(iconSizeDp)
|
.size(iconSizeDp)
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOnlineCoverLoaded) {
|
if (!isOnlineCoverLoaded) {
|
||||||
CoverTextOverlay(
|
CoverTextOverlay(
|
||||||
name = name,
|
name = name,
|
||||||
author = author,
|
author = author,
|
||||||
isNight = isNight
|
isNight = isNight
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -93,4 +94,4 @@ fun Cover(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package io.legado.app.ui.widget.components.cover
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
|
||||||
|
fun buildCoverImageRequest(
|
||||||
|
context: Context,
|
||||||
|
data: Any?,
|
||||||
|
sourceOrigin: String?,
|
||||||
|
loadOnlyWifi: Boolean,
|
||||||
|
crossfade: Boolean = true,
|
||||||
|
configure: ImageRequest.Builder.() -> Unit = {},
|
||||||
|
): ImageRequest {
|
||||||
|
return ImageRequest.Builder(context)
|
||||||
|
.data(data)
|
||||||
|
.crossfade(crossfade)
|
||||||
|
.setParameter("sourceOrigin", sourceOrigin)
|
||||||
|
.setParameter("loadOnlyWifi", loadOnlyWifi)
|
||||||
|
.apply(configure)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
@@ -5,11 +5,19 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
import androidx.compose.material.icons.automirrored.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
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
|
|||||||
+7
-3
@@ -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 = "搜索"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user