[优化] 增加大量 Compose 界面的模糊

This commit is contained in:
HapeLee committed 2026-03-10 00:12:27 +08:00
1 parent 4438543e4d
commit 27f602d9d4
16 files changed
+253 -150

No files matched your search

@@ -44,11 +44,12 @@ sealed interface BaseRuleEvent {
abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiState<T>>( abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiState<T>>(
application: Application, application: Application,
initialState: S, protected val initialState: S,
private val uploadRepository: UploadRepository? = null // 设为可空,提高灵活性 private val uploadRepository: UploadRepository? = null // 设为可空,提高灵活性
) : BaseViewModel(application) { ) : BaseViewModel(application) {
protected val _searchKey = MutableStateFlow("") protected val _searchKey = MutableStateFlow("")
protected val _groupFilter = MutableStateFlow("")
protected val _selectedIds = MutableStateFlow<Set<ID>>(emptySet()) protected val _selectedIds = MutableStateFlow<Set<ID>>(emptySet())
protected val _isSearchMode = MutableStateFlow(false) protected val _isSearchMode = MutableStateFlow(false)
protected val _isUploading = MutableStateFlow(false) protected val _isUploading = MutableStateFlow(false)
@@ -60,24 +61,34 @@ abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiS
abstract val rawDataFlow: Flow<List<Entity>> abstract val rawDataFlow: Flow<List<Entity>>
@Deprecated(
"Use filterData with groupFilter instead",
ReplaceWith("filterData(data, searchKey, \"\")")
)
open fun filterData(data: List<Entity>, key: String): List<Entity> = data open fun filterData(data: List<Entity>, key: String): List<Entity> = data
open fun filterData(data: List<Entity>, searchKey: String, groupFilter: String): List<Entity> {
@Suppress("DEPRECATION")
return filterData(data, if (groupFilter.isNotEmpty()) groupFilter else searchKey)
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private val itemsFlow: Flow<List<T>> by lazy { private val itemsFlow: Flow<List<T>> by lazy {
combine( combine(
rawDataFlow, rawDataFlow,
_searchKey, _searchKey,
_groupFilter,
_localItems _localItems
) { data, key, local -> ) { data, searchKey, groupFilter, local ->
if (local != null && key.isEmpty()) { if (local != null && searchKey.isEmpty() && groupFilter.isEmpty()) {
local local
} else { } else {
filterData(data, key).map { it.toUiItem() } filterData(data, searchKey, groupFilter).map { it.toUiItem() }
} }
} }
} }
val uiState: StateFlow<S> by lazy { open val uiState: StateFlow<S> by lazy {
combine( combine(
itemsFlow, itemsFlow,
_selectedIds, _selectedIds,
@@ -132,6 +143,11 @@ abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiS
_searchKey.value = key ?: "" _searchKey.value = key ?: ""
} }
open fun setGroupFilter(filter: String?) {
_localItems.value = null
_groupFilter.value = filter ?: ""
}
fun setSearchMode(active: Boolean) { fun setSearchMode(active: Boolean) {
_isSearchMode.value = active _isSearchMode.value = active
if (!active) setSearchKey("") if (!active) setSearchKey("")
@@ -18,11 +18,13 @@ 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
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
import io.legado.app.ui.dict.rule.DictRuleViewModel import io.legado.app.ui.dict.rule.DictRuleViewModel
import io.legado.app.ui.main.my.MyViewModel import io.legado.app.ui.main.my.MyViewModel
import io.legado.app.ui.replace.ReplaceEditRoute import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleViewModel import io.legado.app.ui.replace.ReplaceRuleViewModel
import io.legado.app.ui.replace.edit.ReplaceEditViewModel import io.legado.app.ui.replace.edit.ReplaceEditViewModel
import io.legado.app.ui.rss.source.manage.RssSourceViewModel
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModel
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
@@ -43,6 +45,7 @@ val appModule = module {
single<ExploreRepository> { ExploreRepositoryImpl(get()) } single<ExploreRepository> { ExploreRepositoryImpl(get()) }
viewModelOf(::DictRuleViewModel) viewModelOf(::DictRuleViewModel)
viewModelOf(::RssSourceViewModel)
viewModelOf(::ReadRecordViewModel) viewModelOf(::ReadRecordViewModel)
viewModelOf(::ExploreShowViewModel) viewModelOf(::ExploreShowViewModel)
viewModelOf(::MyViewModel) viewModelOf(::MyViewModel)
@@ -50,6 +53,7 @@ val appModule = module {
viewModelOf(::AllBookmarkViewModel) viewModelOf(::AllBookmarkViewModel)
viewModelOf(::TxtTocRuleViewModel) viewModelOf(::TxtTocRuleViewModel)
viewModelOf(::OtherConfigViewModel) viewModelOf(::OtherConfigViewModel)
viewModelOf(::ReadConfigViewModel)
viewModelOf(::TocViewModel) viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel) viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel) viewModelOf(::BookInfoViewModel)
@@ -106,18 +106,14 @@ object ReadBookConfig {
shareConfig = c ?: configList.getOrNull(5) ?: Config() shareConfig = c ?: configList.getOrNull(5) ?: Config()
} }
fun upBg(width: Int, height: Int): Drawable? { fun upBg(width: Int, height: Int) {
val drawable = durConfig.curBgDrawable(width, height) val drawable = durConfig.curBgDrawable(width, height)
if (drawable is BitmapDrawable && drawable.bitmap != null) { if (drawable is BitmapDrawable && drawable.bitmap != null) {
bgMeanColor = drawable.bitmap.getMeanColor() bgMeanColor = drawable.bitmap.getMeanColor()
} else if (drawable is ColorDrawable) { } else if (drawable is ColorDrawable) {
bgMeanColor = drawable.color bgMeanColor = drawable.color
} }
val tmp = bg
bg = drawable bg = drawable
// 返回旧 Drawable,由调用方在视图更新完成后再 recycle,
// 避免视图仍引用旧 BitmapDrawable 时 bitmap 已被 recycle 导致崩溃
return tmp
} }
fun save() { fun save() {
@@ -1154,4 +1150,4 @@ object ReadBookConfig {
return bgDrawable ?: fallbackColor.toDrawable() return bgDrawable ?: fallbackColor.toDrawable()
} }
} }
} }
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -308,13 +307,17 @@ fun TxtRuleScreen(
}) })
} }
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier Box(
modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
contentPadding = PaddingValues(top = 8.dp, bottom = 120.dp), contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 8.dp,
bottom = paddingValues.calculateBottomPadding() + 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(rules, key = { it.id }) { item -> items(rules, key = { it.id }) { item ->
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
@@ -304,13 +303,17 @@ fun DictRuleScreen(
}) })
} }
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier Box(
modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues)) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
contentPadding = PaddingValues(top = 8.dp, bottom = 120.dp), contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 8.dp,
bottom = paddingValues.calculateBottomPadding() + 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(rules, key = { it.id }) { item -> items(rules, key = { it.id }) { item ->
@@ -23,7 +23,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -65,7 +64,10 @@ import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.GroupManageBottomSheet
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.divider.PillDivider
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheetMode import io.legado.app.ui.widget.components.exportComponents.FilePickerSheetMode
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
@@ -252,7 +254,8 @@ fun ReplaceRuleScreen(
GroupManageBottomSheet( GroupManageBottomSheet(
groups = groups, groups = groups,
onDismissRequest = { showGroupManageSheet = false }, onDismissRequest = { showGroupManageSheet = false },
viewModel = viewModel onUpdateGroup = { old, new -> viewModel.upGroup(old, new) },
onDeleteGroup = { viewModel.delGroup(it) }
) )
} }
@@ -332,6 +335,7 @@ fun ReplaceRuleScreen(
.coerceAtLeast(0), .coerceAtLeast(0),
edgePadding = 0.dp, edgePadding = 0.dp,
divider = {}, divider = {},
containerColor = GlassTopAppBarDefaults.containerColor(),
) { ) {
tabItems.forEachIndexed { index, title -> tabItems.forEachIndexed { index, title ->
Tab( Tab(
@@ -403,7 +407,7 @@ fun ReplaceRuleScreen(
text = { Text("帮助") }, text = { Text("帮助") },
onClick = { /*TODO*/ dismiss() } onClick = { /*TODO*/ dismiss() }
) )
HorizontalDivider() PillDivider()
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = { Text("旧的在前") }, text = { Text("旧的在前") },
onClick = { viewModel.setSortMode("asc"); dismiss() } onClick = { viewModel.setSortMode("asc"); dismiss() }
@@ -436,7 +440,6 @@ fun ReplaceRuleScreen(
) { padding -> ) { padding ->
Box( Box(
modifier = Modifier modifier = Modifier
.padding(padding)
.fillMaxSize() .fillMaxSize()
) { ) {
FastScrollLazyColumn( FastScrollLazyColumn(
@@ -444,8 +447,8 @@ fun ReplaceRuleScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize(), .fillMaxSize(),
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 8.dp, top = padding.calculateTopPadding() + 8.dp,
bottom = 120.dp bottom = padding.calculateBottomPadding() + 120.dp
), ),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
@@ -30,9 +30,15 @@ fun Modifier.responsiveHazeEffect(
if (!enableBlur) return this if (!enableBlur) return this
val style = if (enableProgressiveBlur) {
HazeMaterials.ultraThin()
} else {
HazeMaterials.regular()
}
return this.hazeEffect( return this.hazeEffect(
state = state, state = state,
style = HazeMaterials.ultraThin() style = style
) { ) {
if (enableProgressiveBlur) { if (enableProgressiveBlur) {
progressive = HazeProgressive.verticalGradient( progressive = HazeProgressive.verticalGradient(
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -69,6 +70,7 @@ fun EmptyMessageView(
AnimatedTextLine( AnimatedTextLine(
text = message, text = message,
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 2, maxLines = 2,
softWrap = true, softWrap = true,
@@ -43,7 +43,7 @@ object GlassTopAppBarDefaults {
val enableBlur = ThemeConfig.enableBlur val enableBlur = ThemeConfig.enableBlur
val containerColor = if (enableBlur) { val containerColor = if (enableBlur) {
MaterialTheme.colorScheme.surface MaterialTheme.colorScheme.surface.copy(alpha = 0f)
} else { } else {
MaterialTheme.colorScheme.surface.copy(alpha = alpha) MaterialTheme.colorScheme.surface.copy(alpha = alpha)
} }
@@ -60,6 +60,19 @@ object GlassTopAppBarDefaults {
) )
} }
@Composable
fun containerColor(): Color {
val enableBlur = ThemeConfig.enableBlur
val containerColor = if (enableBlur) {
MaterialTheme.colorScheme.surface.copy(alpha = 0f)
} else {
MaterialTheme.colorScheme.surface
}
return containerColor
}
@Composable @Composable
fun controlContainerColor(): Color { fun controlContainerColor(): Color {
val enableBlur = ThemeConfig.enableBlur val enableBlur = ThemeConfig.enableBlur
@@ -72,4 +85,4 @@ object GlassTopAppBarDefaults {
} }
} }
} }
@@ -27,6 +27,7 @@ import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -36,6 +37,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import dev.chrisbanes.haze.HazeState
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.SelectionBottomBar import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.rules.ListUiState import io.legado.app.ui.widget.components.rules.ListUiState
@@ -46,13 +50,14 @@ import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
fun <T> ListScaffold( fun <T> ListScaffold(
title: String, title: String,
state: ListUiState<T>, state: ListUiState<T>,
onBackClick: () -> Unit, subtitle: String? = null,
onBackClick: (() -> Unit)? = null,
onSearchToggle: (Boolean) -> Unit, onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String = "搜索...", searchPlaceholder: String = "搜索...",
topBarActions: @Composable RowScope.() -> Unit = {}, topBarActions: @Composable RowScope.() -> Unit = {},
bottomContent: @Composable (ColumnScope.() -> Unit)? = null, bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null,
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {}, dropDownMenuContent: @Composable (ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
selectionActions: SelectionActions? = null, selectionActions: SelectionActions? = null,
onAddClick: (() -> Unit)? = null, onAddClick: (() -> Unit)? = null,
floatingActionButton: @Composable () -> Unit = { floatingActionButton: @Composable () -> Unit = {
@@ -80,28 +85,36 @@ fun <T> ListScaffold(
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val hazeState = remember { HazeState() }
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = { topBar = {
DynamicTopAppBar( Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
title = title, DynamicTopAppBar(
state = state, title = title,
scrollBehavior = scrollBehavior, subtitle = subtitle,
onBackClick = onBackClick, state = state,
onSearchToggle = onSearchToggle, scrollBehavior = scrollBehavior,
onSearchQueryChange = onSearchQueryChange, onBackClick = onBackClick,
searchPlaceholder = searchPlaceholder, onSearchToggle = onSearchToggle,
onClearSelection = { selectionActions?.onSelectInvert?.invoke() }, // 或其他逻辑 onSearchQueryChange = onSearchQueryChange,
topBarActions = topBarActions, searchPlaceholder = searchPlaceholder,
dropDownMenuContent = dropDownMenuContent, onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
bottomContent = bottomContent topBarActions = topBarActions,
) dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent
)
}
}, },
floatingActionButton = floatingActionButton floatingActionButton = floatingActionButton
) { paddingValues -> ) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) { Box(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState)
) {
content(paddingValues) content(paddingValues)
AnimatedVisibility( AnimatedVisibility(
@@ -124,4 +137,4 @@ fun <T> ListScaffold(
} }
} }
} }
} }
@@ -1,31 +1,104 @@
package io.legado.app.ui.widget.components.menuItem package io.legado.app.ui.widget.components.menuItem
import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.DropdownMenuItem import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuDefaults
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.rememberOpaqueColorScheme import io.legado.app.ui.theme.rememberOpaqueColorScheme
@Composable @Composable
fun RoundDropdownMenuItem( fun RoundDropdownMenuItem(
modifier: Modifier = Modifier,
text: @Composable () -> Unit, text: @Composable () -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource? = null,
) { ) {
val colorScheme = rememberOpaqueColorScheme() val colorScheme = rememberOpaqueColorScheme()
val interaction = interactionSource ?: remember { MutableInteractionSource() }
DropdownMenuItem( val contentColor =
if (enabled) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
Surface(
onClick = onClick,
modifier = modifier modifier = modifier
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)
.clip(MaterialTheme.shapes.small) .fillMaxWidth(),
.background(colorScheme.surface), enabled = enabled,
text = text, shape = MaterialTheme.shapes.small,
trailingIcon = trailingIcon, color = colorScheme.surface,
onClick = onClick interactionSource = interaction
) {
CompositionLocalProvider(
LocalContentColor provides contentColor,
LocalTextStyle provides MaterialTheme.typography.labelLarge
) {
Row(
modifier = Modifier
.padding(contentPadding)
.heightIn(min = 48.dp)
.widthIn(min = 120.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (leadingIcon != null) {
leadingIcon()
Spacer(Modifier.width(8.dp))
}
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.CenterStart
) {
text()
}
if (trailingIcon != null) {
Spacer(Modifier.width(8.dp))
trailingIcon()
}
}
}
}
}
@Composable
fun MenuItemIcon(
imageVector: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null,
tint: Color = LocalContentColor.current
) {
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = modifier.size(18.dp),
tint = tint
) )
} }
@@ -1,17 +1,8 @@
package io.legado.app.ui.widget.components.rules package io.legado.app.ui.widget.components.rules
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
@@ -20,20 +11,17 @@ import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltip import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.animateFloatingActionButton import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -43,27 +31,25 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.legado.app.R import io.legado.app.R
import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.SelectionBottomBar import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar import io.legado.app.ui.widget.components.list.ListScaffold
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun <T> RuleListScaffold( fun <T> RuleListScaffold(
title: String, title: String,
state: ListUiState<T>, state: ListUiState<T>,
subtitle: String? = null,
onBackClick: () -> Unit, onBackClick: () -> Unit,
onSearchToggle: (Boolean) -> Unit, onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String = "搜索...", searchPlaceholder: String = "搜索...",
topBarActions: @Composable RowScope.() -> Unit = {}, topBarActions: @Composable RowScope.() -> Unit = {},
bottomContent: @Composable (ColumnScope.() -> Unit)? = null, bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null,
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {}, dropDownMenuContent: (@Composable ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
onClearSelection: () -> Unit, onClearSelection: () -> Unit,
onSelectAll: () -> Unit, onSelectAll: () -> Unit,
onSelectInvert: () -> Unit, onSelectInvert: () -> Unit,
@@ -94,8 +80,6 @@ fun <T> RuleListScaffold(
snackbarHostState: SnackbarHostState, snackbarHostState: SnackbarHostState,
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showMenu by remember { mutableStateOf(false) }
var showDeleteConfirmDialog by remember { mutableStateOf(false) } var showDeleteConfirmDialog by remember { mutableStateOf(false) }
if (showDeleteConfirmDialog) { if (showDeleteConfirmDialog) {
@@ -124,50 +108,30 @@ fun <T> RuleListScaffold(
) )
} }
Scaffold( ListScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), title = title,
snackbarHost = { SnackbarHost(snackbarHostState) }, subtitle = subtitle,
topBar = { state = state,
DynamicTopAppBar( onBackClick = onBackClick,
title = title, onSearchToggle = onSearchToggle,
state = state, onSearchQueryChange = onSearchQueryChange,
scrollBehavior = scrollBehavior, searchPlaceholder = searchPlaceholder,
onBackClick = onBackClick, topBarActions = topBarActions,
onSearchToggle = onSearchToggle, bottomContent = bottomContent,
onSearchQueryChange = onSearchQueryChange, dropDownMenuContent = dropDownMenuContent,
searchPlaceholder = searchPlaceholder, onAddClick = onAddClick,
onClearSelection = onClearSelection,
topBarActions = topBarActions,
dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent
)
},
floatingActionButton = floatingActionButton, floatingActionButton = floatingActionButton,
content = { paddingValues -> snackbarHostState = snackbarHostState,
Box(modifier = Modifier.fillMaxSize()) { selectionActions = SelectionActions(
content(paddingValues) onSelectAll = onSelectAll,
AnimatedVisibility( onSelectInvert = onSelectInvert,
visible = state.selectedIds.isNotEmpty(), primaryAction = ActionItem(
modifier = Modifier text = stringResource(R.string.delete),
.align(Alignment.BottomCenter) icon = { Icon(Icons.Default.Delete, null) },
.offset(y = -ScreenOffset) onClick = { showDeleteConfirmDialog = true }
.padding(bottom = 16.dp) ),
.zIndex(1f), secondaryActions = selectionSecondaryActions
enter = slideInVertically { it } + fadeIn(), ),
exit = slideOutVertically { it } + fadeOut() content = content
) {
SelectionBottomBar(
onSelectAll = onSelectAll,
onSelectInvert = onSelectInvert,
primaryAction = ActionItem(
text = stringResource(R.string.delete),
icon = { Icon(Icons.Default.Delete, null) },
onClick = { showDeleteConfirmDialog = true }
),
secondaryActions = selectionSecondaryActions
)
}
}
}
) )
} }
@@ -3,13 +3,13 @@ package io.legado.app.ui.widget.components.settingItem
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
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
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@Composable @Composable
fun DropdownListSettingItem( fun DropdownListSettingItem(
@@ -31,7 +31,7 @@ fun DropdownListSettingItem(
onClick = { }, onClick = { },
dropdownMenu = { onDismiss -> dropdownMenu = { onDismiss ->
displayEntries.forEachIndexed { index, display -> displayEntries.forEachIndexed { index, display ->
DropdownMenuItem( RoundDropdownMenuItem(
text = { Text(display) }, text = { Text(display) },
onClick = { onClick = {
onValueChange(entryValues[index]) onValueChange(entryValues[index])
@@ -11,10 +11,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
@@ -30,10 +28,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
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
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.menuItem.RoundDropdownMenu
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
@@ -42,6 +42,7 @@ fun SettingItem(
painter: Painter? = null, painter: Painter? = null,
imageVector: ImageVector? = null, imageVector: ImageVector? = null,
color: Color? = null, color: Color? = null,
shape: Shape = MaterialTheme.shapes.extraSmall,
title: String, title: String,
description: String? = null, description: String? = null,
option: String? = null, option: String? = null,
@@ -58,14 +59,14 @@ fun SettingItem(
val isExpandable = expandContent != null && onExpandChange != null val isExpandable = expandContent != null && onExpandChange != null
GlassCard( GlassCard(
modifier = Modifier modifier = modifier
.fillMaxWidth(), .fillMaxWidth(),
shape = RoundedCornerShape(4.dp), shape = shape,
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow, color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
) { ) {
Column { Column {
ListItem( ListItem(
modifier = modifier modifier = Modifier
.combinedClickable( .combinedClickable(
onClick = { onClick = {
when { when {
@@ -139,7 +140,7 @@ fun SettingItem(
} }
dropdownMenu?.let { menu -> dropdownMenu?.let { menu ->
DropdownMenu( RoundDropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }) { onDismissRequest = { showMenu = false }) {
menu { showMenu = false } menu { showMenu = false }
@@ -167,4 +168,4 @@ fun SettingItem(
} }
} }
} }
} }
@@ -16,7 +16,6 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
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.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -29,6 +28,7 @@ import io.legado.app.ui.widget.components.AdaptiveAnimatedText
import io.legado.app.ui.widget.components.AnimatedTextLine import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.SearchBarSection
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
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.rules.ListUiState import io.legado.app.ui.widget.components.rules.ListUiState
@@ -41,7 +41,7 @@ fun <T> DynamicTopAppBar(
subtitle: String? = null, subtitle: String? = null,
state: ListUiState<T>, state: ListUiState<T>,
scrollBehavior: TopAppBarScrollBehavior, scrollBehavior: TopAppBarScrollBehavior,
onBackClick: () -> Unit, onBackClick: (() -> Unit)? = null,
onSearchToggle: (Boolean) -> Unit, onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit, onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String, searchPlaceholder: String,
@@ -51,7 +51,7 @@ fun <T> DynamicTopAppBar(
onClearSelection: () -> Unit, onClearSelection: () -> Unit,
topBarActions: @Composable RowScope.() -> Unit = {}, topBarActions: @Composable RowScope.() -> Unit = {},
dropDownMenuContent: @Composable (ColumnScope.(dismiss: () -> Unit) -> Unit)? = null, dropDownMenuContent: @Composable (ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
bottomContent: @Composable (ColumnScope.() -> Unit)? = null bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null
) { ) {
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
val isSelecting = state.selectedIds.isNotEmpty() val isSelecting = state.selectedIds.isNotEmpty()
@@ -75,25 +75,31 @@ fun <T> DynamicTopAppBar(
{ AnimatedTextLine(text = it) } { AnimatedTextLine(text = it) }
}, },
navigationIcon = { navigationIcon = {
TopbarNavigationButton( if (isSelecting || onBackClick != null) {
onClick = { if (isSelecting) onClearSelection() else onBackClick() }, TopbarNavigationButton(
imageVector = if (isSelecting) Icons.Default.Close else Icons.AutoMirrored.Filled.ArrowBack, onClick = { if (isSelecting) onClearSelection() else onBackClick?.invoke() },
contentDescription = if (isSelecting) "取消选择" else "返回" imageVector = if (isSelecting) Icons.Default.Close else Icons.AutoMirrored.Filled.ArrowBack,
) contentDescription = if (isSelecting) "取消选择" else "返回"
)
}
}, },
actions = { actions = {
if (!isSelecting) { if (!isSelecting) {
IconButton(onClick = { onSearchToggle(!state.isSearch) }) { TopBarActionButton(
Icon(Icons.Default.Search, null) onClick = { onSearchToggle(!state.isSearch) },
} imageVector = Icons.Default.Search,
contentDescription = "搜索"
)
topBarActions() topBarActions()
dropDownMenuContent?.let { content -> dropDownMenuContent?.let { content ->
Box { Box {
IconButton(onClick = { showMenu = true }) { TopBarActionButton(
Icon(Icons.Default.MoreVert, null) onClick = { showMenu = true },
} imageVector = Icons.Default.MoreVert,
contentDescription = "更多"
)
RoundDropdownMenu( RoundDropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false } onDismissRequest = { showMenu = false }
@@ -122,6 +128,6 @@ fun <T> DynamicTopAppBar(
) )
} }
bottomContent?.invoke(this) bottomContent?.invoke(this, scrollBehavior)
} }
} }
+1 -1
View File
@@ -755,7 +755,7 @@
<string name="default_cover">默认封面</string> <string name="default_cover">默认封面</string>
<string name="restore_ignore">恢复忽略列表</string> <string name="restore_ignore">恢复忽略列表</string>
<string name="restore_ignore_summary">选择恢复忽略内容</string> <string name="restore_ignore_summary">选择恢复忽略内容</string>
<string name="read_config">界面设置</string> <string name="read_config">阅读界面</string>
<string name="group_name">分组名称</string> <string name="group_name">分组名称</string>
<string name="note_content">备注</string> <string name="note_content">备注</string>
<string name="replace_enable_default_t">默认启用替换净化</string> <string name="replace_enable_default_t">默认启用替换净化</string>