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

This commit is contained in:
HapeLee
2026-03-10 00:12:27 +08:00
parent 4438543e4d
commit 27f602d9d4
16 changed files with 253 additions and 150 deletions
@@ -44,11 +44,12 @@ sealed interface BaseRuleEvent {
abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiState<T>>(
application: Application,
initialState: S,
protected val initialState: S,
private val uploadRepository: UploadRepository? = null // 设为可空,提高灵活性
) : BaseViewModel(application) {
protected val _searchKey = MutableStateFlow("")
protected val _groupFilter = MutableStateFlow("")
protected val _selectedIds = MutableStateFlow<Set<ID>>(emptySet())
protected val _isSearchMode = 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>>
@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>, searchKey: String, groupFilter: String): List<Entity> {
@Suppress("DEPRECATION")
return filterData(data, if (groupFilter.isNotEmpty()) groupFilter else searchKey)
}
@OptIn(FlowPreview::class)
private val itemsFlow: Flow<List<T>> by lazy {
combine(
rawDataFlow,
_searchKey,
_groupFilter,
_localItems
) { data, key, local ->
if (local != null && key.isEmpty()) {
) { data, searchKey, groupFilter, local ->
if (local != null && searchKey.isEmpty() && groupFilter.isEmpty()) {
local
} 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(
itemsFlow,
_selectedIds,
@@ -132,6 +143,11 @@ abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiS
_searchKey.value = key ?: ""
}
open fun setGroupFilter(filter: String?) {
_localItems.value = null
_groupFilter.value = filter ?: ""
}
fun setSearchMode(active: Boolean) {
_isSearchMode.value = active
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.rule.TxtTocRuleViewModel
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.main.my.MyViewModel
import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleViewModel
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.viewModel
import org.koin.core.module.dsl.viewModelOf
@@ -43,6 +45,7 @@ val appModule = module {
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
viewModelOf(::DictRuleViewModel)
viewModelOf(::RssSourceViewModel)
viewModelOf(::ReadRecordViewModel)
viewModelOf(::ExploreShowViewModel)
viewModelOf(::MyViewModel)
@@ -50,6 +53,7 @@ val appModule = module {
viewModelOf(::AllBookmarkViewModel)
viewModelOf(::TxtTocRuleViewModel)
viewModelOf(::OtherConfigViewModel)
viewModelOf(::ReadConfigViewModel)
viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel)
@@ -106,18 +106,14 @@ object ReadBookConfig {
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)
if (drawable is BitmapDrawable && drawable.bitmap != null) {
bgMeanColor = drawable.bitmap.getMeanColor()
} else if (drawable is ColorDrawable) {
bgMeanColor = drawable.color
}
val tmp = bg
bg = drawable
// 返回旧 Drawable,由调用方在视图更新完成后再 recycle,
// 避免视图仍引用旧 BitmapDrawable 时 bitmap 已被 recycle 导致崩溃
return tmp
}
fun save() {
@@ -1154,4 +1150,4 @@ object ReadBookConfig {
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -308,13 +307,17 @@ fun TxtRuleScreen(
})
}
) { paddingValues ->
Box(modifier = Modifier
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)) {
) {
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
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)
) {
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@@ -304,13 +303,17 @@ fun DictRuleScreen(
})
}
) { paddingValues ->
Box(modifier = Modifier
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)) {
) {
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
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)
) {
items(rules, key = { it.id }) { item ->
@@ -23,7 +23,6 @@ import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
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.ui.widget.components.ActionItem
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.divider.PillDivider
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.importComponents.BaseImportUiState
@@ -252,7 +254,8 @@ fun ReplaceRuleScreen(
GroupManageBottomSheet(
groups = groups,
onDismissRequest = { showGroupManageSheet = false },
viewModel = viewModel
onUpdateGroup = { old, new -> viewModel.upGroup(old, new) },
onDeleteGroup = { viewModel.delGroup(it) }
)
}
@@ -332,6 +335,7 @@ fun ReplaceRuleScreen(
.coerceAtLeast(0),
edgePadding = 0.dp,
divider = {},
containerColor = GlassTopAppBarDefaults.containerColor(),
) {
tabItems.forEachIndexed { index, title ->
Tab(
@@ -403,7 +407,7 @@ fun ReplaceRuleScreen(
text = { Text("帮助") },
onClick = { /*TODO*/ dismiss() }
)
HorizontalDivider()
PillDivider()
RoundDropdownMenuItem(
text = { Text("旧的在前") },
onClick = { viewModel.setSortMode("asc"); dismiss() }
@@ -436,7 +440,6 @@ fun ReplaceRuleScreen(
) { padding ->
Box(
modifier = Modifier
.padding(padding)
.fillMaxSize()
) {
FastScrollLazyColumn(
@@ -444,8 +447,8 @@ fun ReplaceRuleScreen(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = 8.dp,
bottom = 120.dp
top = padding.calculateTopPadding() + 8.dp,
bottom = padding.calculateBottomPadding() + 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -30,9 +30,15 @@ fun Modifier.responsiveHazeEffect(
if (!enableBlur) return this
val style = if (enableProgressiveBlur) {
HazeMaterials.ultraThin()
} else {
HazeMaterials.regular()
}
return this.hazeEffect(
state = state,
style = HazeMaterials.ultraThin()
style = style
) {
if (enableProgressiveBlur) {
progressive = HazeProgressive.verticalGradient(
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -69,6 +70,7 @@ fun EmptyMessageView(
AnimatedTextLine(
text = message,
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center,
maxLines = 2,
softWrap = true,
@@ -43,7 +43,7 @@ object GlassTopAppBarDefaults {
val enableBlur = ThemeConfig.enableBlur
val containerColor = if (enableBlur) {
MaterialTheme.colorScheme.surface
MaterialTheme.colorScheme.surface.copy(alpha = 0f)
} else {
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
fun controlContainerColor(): Color {
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.TooltipDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
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.unit.dp
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.SelectionBottomBar
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(
title: String,
state: ListUiState<T>,
onBackClick: () -> Unit,
subtitle: String? = null,
onBackClick: (() -> Unit)? = null,
onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String = "搜索...",
topBarActions: @Composable RowScope.() -> Unit = {},
bottomContent: @Composable (ColumnScope.() -> Unit)? = null,
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null,
dropDownMenuContent: @Composable (ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
selectionActions: SelectionActions? = null,
onAddClick: (() -> Unit)? = null,
floatingActionButton: @Composable () -> Unit = {
@@ -80,28 +85,36 @@ fun <T> ListScaffold(
content: @Composable (PaddingValues) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val hazeState = remember { HazeState() }
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
DynamicTopAppBar(
title = title,
state = state,
scrollBehavior = scrollBehavior,
onBackClick = onBackClick,
onSearchToggle = onSearchToggle,
onSearchQueryChange = onSearchQueryChange,
searchPlaceholder = searchPlaceholder,
onClearSelection = { selectionActions?.onSelectInvert?.invoke() }, // 或其他逻辑
topBarActions = topBarActions,
dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent
)
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
DynamicTopAppBar(
title = title,
subtitle = subtitle,
state = state,
scrollBehavior = scrollBehavior,
onBackClick = onBackClick,
onSearchToggle = onSearchToggle,
onSearchQueryChange = onSearchQueryChange,
searchPlaceholder = searchPlaceholder,
onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
topBarActions = topBarActions,
dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent
)
}
},
floatingActionButton = floatingActionButton
) { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState)
) {
content(paddingValues)
AnimatedVisibility(
@@ -124,4 +137,4 @@ fun <T> ListScaffold(
}
}
}
}
}
@@ -1,31 +1,104 @@
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.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.MenuDefaults
import androidx.compose.material3.Surface
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.rememberOpaqueColorScheme
@Composable
fun RoundDropdownMenuItem(
modifier: Modifier = Modifier,
text: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource? = null,
) {
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
.padding(horizontal = 8.dp)
.clip(MaterialTheme.shapes.small)
.background(colorScheme.surface),
text = text,
trailingIcon = trailingIcon,
onClick = onClick
.fillMaxWidth(),
enabled = enabled,
shape = MaterialTheme.shapes.small,
color = colorScheme.surface,
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
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.PaddingValues
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.filled.Add
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.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
@@ -43,27 +31,25 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.list.ListScaffold
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun <T> RuleListScaffold(
title: String,
state: ListUiState<T>,
subtitle: String? = null,
onBackClick: () -> Unit,
onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String = "搜索...",
topBarActions: @Composable RowScope.() -> Unit = {},
bottomContent: @Composable (ColumnScope.() -> Unit)? = null,
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null,
dropDownMenuContent: (@Composable ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
onClearSelection: () -> Unit,
onSelectAll: () -> Unit,
onSelectInvert: () -> Unit,
@@ -94,8 +80,6 @@ fun <T> RuleListScaffold(
snackbarHostState: SnackbarHostState,
content: @Composable (PaddingValues) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showMenu by remember { mutableStateOf(false) }
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
if (showDeleteConfirmDialog) {
@@ -124,50 +108,30 @@ fun <T> RuleListScaffold(
)
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
DynamicTopAppBar(
title = title,
state = state,
scrollBehavior = scrollBehavior,
onBackClick = onBackClick,
onSearchToggle = onSearchToggle,
onSearchQueryChange = onSearchQueryChange,
searchPlaceholder = searchPlaceholder,
onClearSelection = onClearSelection,
topBarActions = topBarActions,
dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent
)
},
ListScaffold(
title = title,
subtitle = subtitle,
state = state,
onBackClick = onBackClick,
onSearchToggle = onSearchToggle,
onSearchQueryChange = onSearchQueryChange,
searchPlaceholder = searchPlaceholder,
topBarActions = topBarActions,
bottomContent = bottomContent,
dropDownMenuContent = dropDownMenuContent,
onAddClick = onAddClick,
floatingActionButton = floatingActionButton,
content = { paddingValues ->
Box(modifier = Modifier.fillMaxSize()) {
content(paddingValues)
AnimatedVisibility(
visible = state.selectedIds.isNotEmpty(),
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = -ScreenOffset)
.padding(bottom = 16.dp)
.zIndex(1f),
enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut()
) {
SelectionBottomBar(
onSelectAll = onSelectAll,
onSelectInvert = onSelectInvert,
primaryAction = ActionItem(
text = stringResource(R.string.delete),
icon = { Icon(Icons.Default.Delete, null) },
onClick = { showDeleteConfirmDialog = true }
),
secondaryActions = selectionSecondaryActions
)
}
}
}
snackbarHostState = snackbarHostState,
selectionActions = SelectionActions(
onSelectAll = onSelectAll,
onSelectInvert = onSelectInvert,
primaryAction = ActionItem(
text = stringResource(R.string.delete),
icon = { Icon(Icons.Default.Delete, null) },
onClick = { showDeleteConfirmDialog = true }
),
secondaryActions = selectionSecondaryActions
),
content = content
)
}
}
@@ -3,13 +3,13 @@ package io.legado.app.ui.widget.components.settingItem
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@Composable
fun DropdownListSettingItem(
@@ -31,7 +31,7 @@ fun DropdownListSettingItem(
onClick = { },
dropdownMenu = { onDismiss ->
displayEntries.forEachIndexed { index, display ->
DropdownMenuItem(
RoundDropdownMenuItem(
text = { Text(display) },
onClick = {
onValueChange(entryValues[index])
@@ -11,10 +11,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
@@ -30,10 +28,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -42,6 +42,7 @@ fun SettingItem(
painter: Painter? = null,
imageVector: ImageVector? = null,
color: Color? = null,
shape: Shape = MaterialTheme.shapes.extraSmall,
title: String,
description: String? = null,
option: String? = null,
@@ -58,14 +59,14 @@ fun SettingItem(
val isExpandable = expandContent != null && onExpandChange != null
GlassCard(
modifier = Modifier
modifier = modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
shape = shape,
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
) {
Column {
ListItem(
modifier = modifier
modifier = Modifier
.combinedClickable(
onClick = {
when {
@@ -139,7 +140,7 @@ fun SettingItem(
}
dropdownMenu?.let { menu ->
DropdownMenu(
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { 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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
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.GlassMediumFlexibleTopAppBar
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.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.rules.ListUiState
@@ -41,7 +41,7 @@ fun <T> DynamicTopAppBar(
subtitle: String? = null,
state: ListUiState<T>,
scrollBehavior: TopAppBarScrollBehavior,
onBackClick: () -> Unit,
onBackClick: (() -> Unit)? = null,
onSearchToggle: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit,
searchPlaceholder: String,
@@ -51,7 +51,7 @@ fun <T> DynamicTopAppBar(
onClearSelection: () -> Unit,
topBarActions: @Composable RowScope.() -> Unit = {},
dropDownMenuContent: @Composable (ColumnScope.(dismiss: () -> Unit) -> Unit)? = null,
bottomContent: @Composable (ColumnScope.() -> Unit)? = null
bottomContent: @Composable (ColumnScope.(TopAppBarScrollBehavior) -> Unit)? = null
) {
var showMenu by remember { mutableStateOf(false) }
val isSelecting = state.selectedIds.isNotEmpty()
@@ -75,25 +75,31 @@ fun <T> DynamicTopAppBar(
{ AnimatedTextLine(text = it) }
},
navigationIcon = {
TopbarNavigationButton(
onClick = { if (isSelecting) onClearSelection() else onBackClick() },
imageVector = if (isSelecting) Icons.Default.Close else Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = if (isSelecting) "取消选择" else "返回"
)
if (isSelecting || onBackClick != null) {
TopbarNavigationButton(
onClick = { if (isSelecting) onClearSelection() else onBackClick?.invoke() },
imageVector = if (isSelecting) Icons.Default.Close else Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = if (isSelecting) "取消选择" else "返回"
)
}
},
actions = {
if (!isSelecting) {
IconButton(onClick = { onSearchToggle(!state.isSearch) }) {
Icon(Icons.Default.Search, null)
}
TopBarActionButton(
onClick = { onSearchToggle(!state.isSearch) },
imageVector = Icons.Default.Search,
contentDescription = "搜索"
)
topBarActions()
dropDownMenuContent?.let { content ->
Box {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, null)
}
TopBarActionButton(
onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert,
contentDescription = "更多"
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
@@ -122,6 +128,6 @@ fun <T> DynamicTopAppBar(
)
}
bottomContent?.invoke(this)
bottomContent?.invoke(this, scrollBehavior)
}
}
}