From 27f602d9d4a2e3cf97ac33e1fb9205d8ce66c3e6 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Tue, 10 Mar 2026 00:12:00 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E5=A4=A7=E9=87=8F=20Compose=20=E7=95=8C=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E6=A8=A1=E7=B3=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/legado/app/base/BaseRuleViewModel.kt | 26 ++++- .../main/java/io/legado/app/di/appModule.kt | 4 + .../legado/app/help/config/ReadBookConfig.kt | 8 +- .../app/ui/book/toc/rule/TxtRuleScreen.kt | 11 ++- .../legado/app/ui/dict/rule/DictRuleScreen.kt | 11 ++- .../app/ui/replace/ReplaceRuleScreen.kt | 15 +-- .../java/io/legado/app/ui/theme/HazeStyle.kt | 8 +- .../ui/widget/components/EmptyMessageView.kt | 2 + .../GlassMediumFlexibleTopAppBar.kt | 17 +++- .../ui/widget/components/list/ListScaffold.kt | 49 ++++++---- .../menuItem/RoundDropdownMenuItem.kt | 95 +++++++++++++++--- .../components/rules/RuleListScaffold.kt | 98 ++++++------------- .../components/settingItem/ListSettingItem.kt | 4 +- .../components/settingItem/SettingItem.kt | 15 +-- .../components/topbar/DynamicTopAppBar.kt | 38 ++++--- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 16 files changed, 253 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/io/legado/app/base/BaseRuleViewModel.kt b/app/src/main/java/io/legado/app/base/BaseRuleViewModel.kt index 25f304052..e9c0c5ee3 100644 --- a/app/src/main/java/io/legado/app/base/BaseRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/base/BaseRuleViewModel.kt @@ -44,11 +44,12 @@ sealed interface BaseRuleEvent { abstract class BaseRuleViewModel, Entity, ID, S : ListUiState>( 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>(emptySet()) protected val _isSearchMode = MutableStateFlow(false) protected val _isUploading = MutableStateFlow(false) @@ -60,24 +61,34 @@ abstract class BaseRuleViewModel, Entity, ID, S : ListUiS abstract val rawDataFlow: Flow> + @Deprecated( + "Use filterData with groupFilter instead", + ReplaceWith("filterData(data, searchKey, \"\")") + ) open fun filterData(data: List, key: String): List = data + open fun filterData(data: List, searchKey: String, groupFilter: String): List { + @Suppress("DEPRECATION") + return filterData(data, if (groupFilter.isNotEmpty()) groupFilter else searchKey) + } + @OptIn(FlowPreview::class) private val itemsFlow: Flow> 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 by lazy { + open val uiState: StateFlow by lazy { combine( itemsFlow, _selectedIds, @@ -132,6 +143,11 @@ abstract class BaseRuleViewModel, 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("") diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index 77bf1499c..1b8e41d9a 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -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 { 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) diff --git a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt index c0d9e8bc7..a680f6e8a 100644 --- a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt @@ -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() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt index b43849852..373876a60 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt @@ -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 -> diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt index 0a2ce505c..e61eff930 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt @@ -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 -> diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt index 5600611e7..6750f9870 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt @@ -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) ) { diff --git a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt index 36f808ae1..2b9ad664d 100644 --- a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt +++ b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt b/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt index cb7b7b980..a5de35e2d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt @@ -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, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt index b1b29846a..6926cf269 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt @@ -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 { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt index 30bbfa4ee..442437053 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt @@ -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 ListScaffold( title: String, state: ListUiState, - 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 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 ListScaffold( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt index fe61216a0..560b6b4d7 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt @@ -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 ) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt index 994ef67c6..3e2960263 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt @@ -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 RuleListScaffold( title: String, state: ListUiState, + 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 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 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 ) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt index 1bc13b705..724b40289 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ListSettingItem.kt @@ -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]) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt index f6887f54c..c161e19c7 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SettingItem.kt @@ -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( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt index aa01bc162..c7fe1b7b6 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt @@ -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 DynamicTopAppBar( subtitle: String? = null, state: ListUiState, scrollBehavior: TopAppBarScrollBehavior, - onBackClick: () -> Unit, + onBackClick: (() -> Unit)? = null, onSearchToggle: (Boolean) -> Unit, onSearchQueryChange: (String) -> Unit, searchPlaceholder: String, @@ -51,7 +51,7 @@ fun 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 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 DynamicTopAppBar( ) } - bottomContent?.invoke(this) + bottomContent?.invoke(this, scrollBehavior) } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2d73977a9..a9e8c36c6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -755,7 +755,7 @@ 默认封面 恢复忽略列表 选择恢复忽略内容 - 界面设置 + 阅读界面 分组名称 备注 默认启用替换净化