[优化] 增加大量 Compose 界面的模糊
This commit is contained in:
@@ -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,
|
||||
|
||||
+15
-2
@@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+84
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user