[优化] 依旧修复一些Miuix主题的稳定性和界面问题,hmm议题依旧没时间修

This commit is contained in:
HapeLee
2026-04-07 01:00:46 +08:00
parent b2f497c845
commit 37c80e1230
82 changed files with 3265 additions and 2417 deletions
@@ -31,7 +31,7 @@ import io.legado.app.constant.AppConst.appInfo
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.settingItem.SettingItem
import io.legado.app.ui.widget.components.text.AppText
@@ -60,7 +60,7 @@ fun AboutScreen(
GlassMediumFlexibleTopAppBar(
title = stringResource(R.string.about),
navigationIcon = {
TopbarNavigationButton(onClick = onBack)
TopBarNavigationButton(onClick = onBack)
},
scrollBehavior = scrollBehavior
)
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -13,12 +14,10 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -31,6 +30,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.LogUtils
@@ -82,24 +82,28 @@ fun AppLogSheet(
}
}
showStackTrace?.let { stackTrace ->
AlertDialog(
onDismissRequest = { showStackTrace = null },
title = { AppText("Log") },
text = {
SelectionContainer {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
AppText(text = stackTrace, style = LegadoTheme.typography.bodySmall)
}
}
},
confirmButton = {
TextButton(onClick = { showStackTrace = null }) {
AppText(stringResource(android.R.string.ok))
AppAlertDialog(
data = showStackTrace,
onDismissRequest = { showStackTrace = null },
title = "Log",
content = { stackTrace ->
SelectionContainer {
Column(
modifier = Modifier
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
) {
AppText(
text = stackTrace,
style = LegadoTheme.typography.bodySmall
)
}
}
)
}
},
confirmText = stringResource(android.R.string.ok),
onConfirm = { showStackTrace = null }
)
}
@Composable
@@ -6,11 +6,16 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -24,33 +29,46 @@ import androidx.compose.material.icons.filled.UnfoldLess
import androidx.compose.material.icons.filled.UnfoldMore
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
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.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.lazylist.Scroller
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import top.yukonga.miuix.kmp.theme.MiuixTheme
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
ExperimentalMaterial3ExpressiveApi::class
@@ -71,6 +89,7 @@ fun AllBookmarkScreen(
val searchText = uiState.searchQuery
val collapsedGroups = uiState.collapsedGroups
val bookmarksGrouped = uiState.bookmarks
val bookmarkGroups = remember(bookmarksGrouped) { bookmarksGrouped.entries.toList() }
val allKeys = bookmarksGrouped.keys
val isAllCollapsed =
allKeys.isNotEmpty() && allKeys.all { collapsedGroups.contains(it.toString()) }
@@ -82,6 +101,21 @@ fun AllBookmarkScreen(
var showBottomSheet by remember { mutableStateOf(false) }
var pendingExportIsMd by remember { mutableStateOf(false) }
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val stickyGroup by remember(bookmarkGroups, collapsedGroups, listState) {
derivedStateOf {
val firstVisibleIndex = listState.firstVisibleItemIndex
val firstVisibleGroup = bookmarkGroups.getOrNull(firstVisibleIndex)
?: return@derivedStateOf null
val isCollapsed = collapsedGroups.contains(firstVisibleGroup.key.toString())
val shouldStick = firstVisibleIndex > 0 || listState.firstVisibleItemScrollOffset > 24
if (!isCollapsed && shouldStick) {
firstVisibleGroup.key
} else {
null
}
}
}
val exportLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree()
@@ -100,28 +134,31 @@ fun AllBookmarkScreen(
title = "所有书签",
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBack)
TopBarNavigationButton(onClick = onBack)
},
actions = {
if (bookmarksGrouped.isNotEmpty()) {
IconButton(onClick = { viewModel.toggleAllCollapse(allKeys) }) {
Icon(
imageVector = if (isAllCollapsed) Icons.Default.UnfoldMore else Icons.Default.UnfoldLess,
contentDescription = null
)
}
}
IconButton(onClick = {
showSearch = !showSearch
if (!showSearch) {
viewModel.onSearchQueryChanged("")
}
}) {
Icon(Icons.Default.Search, contentDescription = "Search")
}
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "Menu")
TopBarActionButton(
onClick = { viewModel.toggleAllCollapse(allKeys) },
imageVector = if (isAllCollapsed) Icons.Default.UnfoldMore else Icons.Default.UnfoldLess,
contentDescription = null
)
}
TopBarActionButton(
onClick = {
showSearch = !showSearch
if (!showSearch) {
viewModel.onSearchQueryChanged("")
}
},
imageVector = Icons.Default.Search,
contentDescription = null
)
TopBarActionButton(
onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu"
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
@@ -147,6 +184,7 @@ fun AllBookmarkScreen(
)
AnimatedVisibility(
modifier = Modifier.adaptiveHorizontalPadding(),
visible = showSearch,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
@@ -182,7 +220,6 @@ fun AllBookmarkScreen(
}
"EMPTY" -> {
EmptyMessageView(
message = "没有书签!",
modifier = Modifier
@@ -191,46 +228,87 @@ fun AllBookmarkScreen(
}
"CONTENT" -> {
FastScrollLazyColumn(
state = listState,
Box(
modifier = Modifier.fillMaxSize()
) {
uiState.bookmarks.forEach { (headerKey, bookmarks) ->
val isCollapsed = collapsedGroups.contains(headerKey.toString())
FastScrollLazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
) {
items(
items = bookmarkGroups,
key = { it.key.toString() }
) { (headerKey, bookmarks) ->
val isCollapsed = collapsedGroups.contains(headerKey.toString())
stickyHeader(key = "${Scroller.STICKY_HEADER_KEY_PREFIX}${headerKey}") {
CollapsibleHeader(
modifier = Modifier.animateItem(),
title = headerKey.bookName,
subtitle = headerKey.bookAuthor,
isCollapsed = isCollapsed,
onToggle = { viewModel.toggleGroupCollapse(headerKey) }
)
}
if (!isCollapsed) {
items(
items = bookmarks,
key = { it.id }
) { bookmarkUi ->
BookmarkItem(
bookmark = bookmarkUi.rawBookmark,
modifier = Modifier
.animateItem()
.fillMaxWidth(),
isDur = false,
onClick = {
editingBookmark = bookmarkUi.rawBookmark
showBottomSheet = true
},
onLongClick = {
editingBookmark = bookmarkUi.rawBookmark
showBottomSheet = true
}
GlassCard(
modifier = Modifier
.animateItem()
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
containerColor = LegadoTheme.colorScheme.surfaceContainer
) {
BookmarkGroupHeaderContent(
title = headerKey.bookName,
subtitle = headerKey.bookAuthor,
isCollapsed = isCollapsed,
onToggle = { viewModel.toggleGroupCollapse(headerKey) },
isMiuix = isMiuix
)
AnimatedVisibility(
visible = !isCollapsed && bookmarks.isNotEmpty()
) {
Column() {
HorizontalDivider(
color = LegadoTheme.colorScheme.surface
)
bookmarks.forEach { bookmarkUi ->
BookmarkItem(
bookmark = bookmarkUi.rawBookmark,
modifier = Modifier.fillMaxWidth(),
isDur = false,
onClick = {
editingBookmark = bookmarkUi.rawBookmark
showBottomSheet = true
},
onLongClick = {
editingBookmark = bookmarkUi.rawBookmark
showBottomSheet = true
}
)
}
}
}
}
}
}
TopFloatingStickyItem(
item = stickyGroup,
modifier = Modifier
.padding(top = 4.dp, start = 8.dp)
) { group ->
TextCard(
text = group.bookName,
textStyle = LegadoTheme.typography.labelLarge,
backgroundColor = LegadoTheme.colorScheme.cardContainer,
contentColor = LegadoTheme.colorScheme.onCardContainer,
cornerRadius = 8.dp,
horizontalPadding = 8.dp,
verticalPadding = 6.dp,
onClick = {
scope.launch {
val index =
bookmarkGroups.indexOfFirst { it.key == group }
if (index >= 0) {
listState.animateScrollToItem(index)
}
}
}
)
}
}
}
}
@@ -255,3 +333,47 @@ fun AllBookmarkScreen(
)
}
}
@Composable
private fun BookmarkGroupHeaderContent(
modifier: Modifier = Modifier,
title: String,
subtitle: String?,
isCollapsed: Boolean,
onToggle: () -> Unit,
isMiuix: Boolean
) {
val contentColor by animateColorAsState(
if (isMiuix) MiuixTheme.colorScheme.primary else MaterialTheme.colorScheme.primary,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
label = "CardColor"
)
ListItem(
modifier = modifier
.fillMaxWidth()
.combinedClickable(onClick = onToggle),
colors = ListItemDefaults.colors(
containerColor = Color.Transparent,
supportingColor = LegadoTheme.colorScheme.onSurfaceVariant,
trailingIconColor = LegadoTheme.colorScheme.onSurfaceVariant
),
headlineContent = {
AppText(
text = title,
style = LegadoTheme.typography.titleMedium,
color = contentColor
)
},
supportingContent = {
subtitle?.let {
AppText(
text = it,
style = LegadoTheme.typography.labelMedium,
color = LegadoTheme.colorScheme.onSurfaceVariant
)
}
}
)
}
@@ -82,7 +82,7 @@ import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.button.AnimatedTextButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.explore.ExploreKindItem
@@ -296,7 +296,7 @@ fun ExploreShowScreen(
),
title = selectedTitle ?: title,
navigationIcon = {
TopbarNavigationButton(onClick = onBack)
TopBarNavigationButton(onClick = onBack)
},
actions = {
Row(
@@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.cover.BookCover
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
@@ -115,7 +115,10 @@ fun GroupEditContent(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Cover(
BookCover(
name = null,
author = null,
sourceOrigin = group?.groupName,
path = coverPath,
modifier = Modifier
.width(96.dp)
@@ -83,6 +83,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.card.GlassCard
@@ -154,51 +155,39 @@ fun RemoteBookScreen(
}
}
dialogState?.let { state ->
when (state) {
is RemoteBookDialog.DownloadArchive -> {
AlertDialog(
onDismissRequest = { dialogState = null },
title = { AppText(stringResource(R.string.draw)) },
text = { AppText(stringResource(R.string.archive_not_found)) },
confirmButton = {
OutlinedButton(onClick = {
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
dialogState = null
}) {
AppText(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { dialogState = null }) {
AppText(stringResource(android.R.string.cancel))
}
}
)
}
is RemoteBookDialog.ReImport -> {
AlertDialog(
onDismissRequest = { dialogState = null },
title = { AppText("是否重新加入书架?") },
text = { AppText("将会覆盖书籍") },
confirmButton = {
OutlinedButton(onClick = {
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
dialogState = null
}) {
AppText(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { dialogState = null }) {
AppText(stringResource(android.R.string.cancel))
}
}
)
}
}
}
AppAlertDialog(
data = dialogState as? RemoteBookDialog.DownloadArchive,
onDismissRequest = { dialogState = null },
title = stringResource(R.string.draw),
content = {
AppText(stringResource(R.string.archive_not_found))
},
confirmText = stringResource(android.R.string.ok),
onConfirm = { state ->
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
dialogState = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { dialogState = null }
)
AppAlertDialog(
data = dialogState as? RemoteBookDialog.ReImport,
onDismissRequest = { dialogState = null },
title = "是否重新加入书架?",
content = {
AppText("将会覆盖书籍")
},
confirmText = stringResource(android.R.string.ok),
onConfirm = { state ->
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
dialogState = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { dialogState = null }
)
AppModalBottomSheet(
show = showSheet != null,
@@ -378,7 +367,7 @@ private fun ServersSheetContent(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -430,16 +419,16 @@ private fun ServerItem(
onDelete: (() -> Unit)? = null
) {
val containerColor = if (isSelected) {
MaterialTheme.colorScheme.secondaryContainer
LegadoTheme.colorScheme.secondaryContainer
} else {
MaterialTheme.colorScheme.surface
LegadoTheme.colorScheme.surface
}
Card(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
colors = CardDefaults.cardColors(
containerColor = containerColor
@@ -466,7 +455,7 @@ private fun ServerItem(
AppText(
text = it,
style = LegadoTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = LegadoTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -512,7 +501,6 @@ private fun ServerConfigSheetContent(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
AppText(
@@ -628,9 +616,9 @@ private fun PathNavigationBar(
style = LegadoTheme.typography.labelSmall,
fontWeight = if (isLast) FontWeight.SemiBold else FontWeight.Medium,
color = if (isLast)
MaterialTheme.colorScheme.primary
LegadoTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurfaceVariant,
LegadoTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.then(
@@ -646,7 +634,7 @@ private fun PathNavigationBar(
imageVector = Icons.Default.ChevronRight,
contentDescription = null,
modifier = Modifier.size(12.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
tint = LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
)
}
}
@@ -20,16 +20,11 @@ import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.ImageSearch
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -46,11 +41,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.ui.book.changecover.ChangeCoverDialog
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
import io.legado.app.ui.widget.components.button.TopBarButtonVariant
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.cover.BookCover
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.utils.SelectImageContract
@@ -73,13 +71,13 @@ fun BookInfoEditScreen(
GlassMediumFlexibleTopAppBar(
title = stringResource(id = R.string.book_info_edit),
navigationIcon = {
TopbarNavigationButton(
TopBarNavigationButton(
onClick = onBack,
style = TopBarButtonVariant.Outlined
)
},
actions = {
TopbarNavigationButton(
TopBarNavigationButton(
onClick = { viewModel.save(onSave) },
imageVector = Icons.Default.Save
)
@@ -125,11 +123,12 @@ fun BookInfoEditContent(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Cover(
BookCover(
name = uiState.name,
author = uiState.author,
path = uiState.coverUrl,
modifier = Modifier
.width(110.dp)
.height(154.dp)
)
Column(
modifier = Modifier.weight(1f),
@@ -137,7 +136,7 @@ fun BookInfoEditContent(
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(
MediumOutlinedIconButton(
onClick = {
(context as? BookInfoEditActivity)?.showDialogFragment(
ChangeCoverDialog(
@@ -146,31 +145,16 @@ fun BookInfoEditContent(
)
)
},
shapes = ButtonDefaults.shapes()
) {
Icon(
Icons.Default.ImageSearch,
contentDescription = stringResource(id = R.string.default_cover)
)
}
OutlinedButton(
icon = Icons.Default.ImageSearch
)
MediumOutlinedIconButton(
onClick = { selectCover.launch() },
shapes = ButtonDefaults.shapes()
) {
Icon(
Icons.Default.FolderOpen,
contentDescription = stringResource(id = R.string.default_cover)
)
}
OutlinedButton(
icon = Icons.Default.FolderOpen
)
MediumOutlinedIconButton(
onClick = { viewModel.resetCover() },
shapes = ButtonDefaults.shapes()
) {
Icon(
Icons.Default.Replay,
contentDescription = stringResource(id = R.string.default_cover)
)
}
icon = Icons.Default.Replay
)
}
Spacer(modifier = Modifier.height(4.dp))
BookTypeDropdown(
@@ -188,38 +172,38 @@ fun BookInfoEditContent(
onCheckedChange = { viewModel.onFixedTypeChange(it) }
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
AppTextField(
value = uiState.name,
onValueChange = { viewModel.onNameChange(it) },
label = { AppText("书名") },
label = "书名",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
AppTextField(
value = uiState.author,
onValueChange = { viewModel.onAuthorChange(it) },
label = { AppText("作者") },
label = "作者",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
AppTextField(
value = uiState.coverUrl ?: "",
onValueChange = { viewModel.onCoverUrlChange(it) },
label = { AppText("封面链接") },
label = "封面链接",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
AppTextField(
value = uiState.intro ?: "",
onValueChange = { viewModel.onIntroChange(it) },
label = { AppText("简介") },
label = "简介",
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
AppTextField(
value = uiState.remark ?: "",
onValueChange = { viewModel.onRemarkChange(it) },
label = { AppText("备注") },
label = "备注",
modifier = Modifier.fillMaxWidth()
)
}
@@ -247,11 +231,11 @@ fun BookTypeDropdown(
expanded = expanded,
onExpandedChange = { expanded = !expanded }
) {
OutlinedTextField(
AppTextField(
state = textFieldState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { AppText("书籍类型") },
label = "书籍类型",
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
},
@@ -263,13 +247,13 @@ fun BookTypeDropdown(
),
)
ExposedDropdownMenu(
RoundDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
bookTypes.forEach { option ->
DropdownMenuItem(
text = { AppText(option) },
RoundDropdownMenuItem(
text = option,
onClick = {
onTypeSelected(option)
expanded = false
@@ -74,14 +74,17 @@ import cn.hutool.core.date.DateUtil
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.AlertButton
import io.legado.app.ui.widget.components.button.AppIconButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarTopBar
import io.legado.app.ui.widget.components.heatmap.HeatmapConfig
@@ -160,7 +163,7 @@ fun ReadRecordScreen(
)
},
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
},
actions = {
AppIconButton(onClick = {
@@ -192,7 +195,10 @@ fun ReadRecordScreen(
scrollBehavior = scrollBehavior
)
AnimatedVisibility(visible = showSearch) {
AnimatedVisibility(
modifier = Modifier.adaptiveHorizontalPadding(),
visible = showSearch
) {
SearchBarSection(
query = state.searchKey ?: "",
onQueryChange = { viewModel.setSearchKey(it) }
@@ -264,57 +270,34 @@ fun ReadRecordScreen(
}
}
if (pendingDeleteAction != null) {
var skipDeleteConfirmTemp by remember { mutableStateOf(false) }
AlertDialog(
onDismissRequest = {
pendingDeleteAction = null
},
title = { AppText("确认删除") },
text = {
Column {
AppText("确定要删除这条记录吗?")
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (skipDeleteConfirmTemp) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent,
shape = MaterialTheme.shapes.small
)
.clickable { skipDeleteConfirmTemp = !skipDeleteConfirmTemp }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = skipDeleteConfirmTemp, onCheckedChange = null)
AppText(
text = "不再提示",
modifier = Modifier.padding(start = 8.dp),
style = LegadoTheme.typography.bodyMedium
)
}
}
},
confirmButton = {
AlertButton(
onClick = {
pendingDeleteAction?.invoke()
pendingDeleteAction = null
skipDeleteConfirm = skipDeleteConfirmTemp
},
text = "删除"
var skipDeleteConfirmTemp by remember(pendingDeleteAction != null) { mutableStateOf(false) }
AppAlertDialog(
data = pendingDeleteAction,
onDismissRequest = { pendingDeleteAction = null },
title = "确认删除",
content = { _ ->
Column {
AppText("确定要删除这条记录吗?")
Spacer(modifier = Modifier.height(8.dp))
CheckboxItem(
title = "不再提示",
checked = skipDeleteConfirmTemp,
onCheckedChange = { skipDeleteConfirmTemp = it }
)
},
dismissButton = {
TextButton(onClick = {
pendingDeleteAction = null
}) {
AppText("取消")
}
}
)
}
},
confirmText = "删除",
onConfirm = { action ->
action.invoke()
pendingDeleteAction = null
skipDeleteConfirm = skipDeleteConfirmTemp
},
dismissText = "取消",
onDismiss = {
pendingDeleteAction = null
}
)
AppModalBottomSheet(
show = showCalendar,
@@ -335,65 +318,52 @@ fun ReadRecordScreen(
)
}
mergeDialogData?.let { (targetRecord, candidates) ->
var selectedAuthors by remember(targetRecord, candidates) {
mutableStateOf(candidates.map { it.bookAuthor }.toSet())
}
AlertDialog(
onDismissRequest = { mergeDialogData = null },
title = { AppText("合并阅读记录") },
text = {
Column {
AppText("将以下作者的“${targetRecord.bookName}”合并到 ${targetRecord.bookAuthor.ifBlank { "未知作者" }}")
Spacer(modifier = Modifier.height(8.dp))
candidates.forEach { candidate ->
val author = candidate.bookAuthor.ifBlank { "未知作者" }
val isChecked = selectedAuthors.contains(candidate.bookAuthor)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
selectedAuthors =
if (isChecked) selectedAuthors - candidate.bookAuthor
else selectedAuthors + candidate.bookAuthor
}
.padding(vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = isChecked, onCheckedChange = null)
AppText(
text = "$author${formatDuring(candidate.readTime)}",
modifier = Modifier.padding(start = 8.dp),
style = LegadoTheme.typography.bodyMedium
)
}
}
}
},
confirmButton = {
TextButton(
onClick = {
viewModel.mergeReadRecords(
targetRecord,
candidates.filter { selectedAuthors.contains(it.bookAuthor) }
)
mergeDialogData = null
}
) {
AppText("合并")
}
},
dismissButton = {
TextButton(onClick = { mergeDialogData = null }) {
AppText("取消")
}
}
var selectedAuthors by remember(mergeDialogData != null) {
mutableStateOf(
mergeDialogData?.let { (_, candidates) ->
candidates.map { it.bookAuthor }.toSet()
} ?: emptySet()
)
}
AppAlertDialog(
data = mergeDialogData,
onDismissRequest = { mergeDialogData = null },
title = "合并阅读记录",
content = { (targetRecord, candidates) ->
Column {
AppText("将以下作者的“${targetRecord.bookName}”合并到 ${targetRecord.bookAuthor.ifBlank { "未知作者" }}")
Spacer(modifier = Modifier.height(8.dp))
candidates.forEach { candidate ->
val author = candidate.bookAuthor.ifBlank { "未知作者" }
val isChecked = selectedAuthors.contains(candidate.bookAuthor)
CheckboxItem(
title = "$author${formatDuring(candidate.readTime)}",
checked = isChecked,
onCheckedChange = { checked ->
selectedAuthors = if (checked) {
selectedAuthors + candidate.bookAuthor
} else {
selectedAuthors - candidate.bookAuthor
}
}
)
}
}
},
confirmText = "合并",
onConfirm = { (targetRecord, candidates) ->
viewModel.mergeReadRecords(
targetRecord,
candidates.filter { selectedAuthors.contains(it.bookAuthor) }
)
mergeDialogData = null
},
dismissText = "取消",
onDismiss = { mergeDialogData = null }
)
}
@Composable
@@ -590,7 +560,7 @@ fun LazyListScope.renderListByMode(
modifier = Modifier.animateItem(),
startAction = SwipeAction(
icon = Icons.Default.Delete,
background = MaterialTheme.colorScheme.error,
background = LegadoTheme.colorScheme.error,
onSwipe = {
onConfirmDelete { viewModel.deleteDetail(detail) }
}
@@ -613,7 +583,7 @@ fun LazyListScope.renderListByMode(
modifier = Modifier.animateItem(),
startAction = SwipeAction(
icon = Icons.Default.Delete,
background = MaterialTheme.colorScheme.error,
background = LegadoTheme.colorScheme.error,
onSwipe = {
onConfirmDelete { viewModel.deleteSession(session) }
}
@@ -635,14 +605,14 @@ fun LazyListScope.renderListByMode(
modifier = Modifier.animateItem(),
startAction = SwipeAction(
icon = Icons.Default.Delete,
background = MaterialTheme.colorScheme.error,
background = LegadoTheme.colorScheme.error,
onSwipe = {
onConfirmDelete { viewModel.deleteReadRecord(record) }
}
),
endAction = SwipeAction(
icon = Icons.Default.Merge,
background = MaterialTheme.colorScheme.primary,
background = LegadoTheme.colorScheme.primary,
onSwipe = {
onMergeClick(record)
}
@@ -693,7 +663,7 @@ fun LatestReadItem(
AppText(
text = record.bookAuthor.ifBlank { "未知作者" },
style = LegadoTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
color = LegadoTheme.colorScheme.outline,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -36,19 +36,15 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -64,14 +60,18 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.SearchContentHistory
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.button.MediumOutlinedButton
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
@@ -143,10 +143,9 @@ fun SearchContentScreen(
title = if (searchQuery.isNotBlank() && searchResults.isNotEmpty()) {
"${searchResults.size} 条结果"
} else "搜索内容",
navigationIcon = { TopbarNavigationButton(onClick = onBack) },
navigationIcon = { TopBarNavigationButton(onClick = onBack) },
actions = {
Row(
modifier = Modifier.padding(end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
TopBarAnimatedActionButton(
@@ -170,11 +169,16 @@ fun SearchContentScreen(
},
scrollBehavior = scrollBehavior
)
SearchBarSection(
query = searchQuery,
scrollState = listState,
onQueryChange = { viewModel.onQueryChange(it) }
)
Box(
modifier = Modifier.adaptiveHorizontalPadding()
) {
SearchBarSection(
query = searchQuery,
scrollState = listState,
onQueryChange = { viewModel.onQueryChange(it) }
)
}
AnimatedVisibility(visible = isSearching) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
@@ -182,39 +186,28 @@ fun SearchContentScreen(
},
floatingActionButton = {
val fabVisible = (isSearching || searchResults.isNotEmpty()) && searchQuery.isNotBlank()
TooltipBox(
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Above
AppFloatingActionButton(
modifier = Modifier.animateFloatingActionButton(
visible = fabVisible,
alignment = Alignment.BottomEnd,
),
tooltip = {
PlainTooltip {
AppText(if (isSearching) "停止搜索" else "跳转到当前章节")
onClick = {
if (isSearching) {
viewModel.stopSearch()
} else {
scrollToCurrentChapter()
}
},
state = rememberTooltipState(),
tooltipText = if (isSearching) "停止搜索" else "跳转到当前章节"
) {
FloatingActionButton(
modifier = Modifier.animateFloatingActionButton(
visible = fabVisible,
alignment = Alignment.BottomEnd,
),
onClick = {
if (isSearching) {
viewModel.stopSearch()
} else {
scrollToCurrentChapter()
}
}
) {
AnimatedContent(
targetState = isSearching,
label = "FabIconTransition"
) { searching ->
if (searching) {
Icon(Icons.Default.Stop, contentDescription = "停止搜索")
} else {
Icon(Icons.Default.MyLocation, contentDescription = "定位当前章节")
}
AnimatedContent(
targetState = isSearching,
label = "FabIconTransition"
) { searching ->
if (searching) {
AppIcon(Icons.Default.Stop, contentDescription = "停止搜索")
} else {
AppIcon(Icons.Default.MyLocation, contentDescription = "定位当前章节")
}
}
}
@@ -305,7 +298,8 @@ fun SearchHistoryList(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.adaptiveHorizontalPadding()
.padding(vertical = 4.dp),
) {
AppText(
text = "搜索历史",
@@ -354,7 +348,11 @@ fun SearchHistoryList(
icon = Icons.Default.Close,
contentDescription = "删除"
)
}
},
colors = ListItemDefaults.colors(
containerColor = LegadoTheme.colorScheme.surface,
contentColor = LegadoTheme.colorScheme.onSurface
)
)
}
item {
@@ -365,18 +363,12 @@ fun SearchHistoryList(
.animateItem(),
contentAlignment = Alignment.Center
) {
OutlinedButton(
MediumOutlinedButton(
onClick = onClearHistory,
modifier = Modifier.fillMaxWidth(0.6f)
) {
Icon(
imageVector = Icons.Outlined.DeleteSweep,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
AppText("清除搜索历史")
}
modifier = Modifier.fillMaxWidth(0.6f),
icon = Icons.Outlined.DeleteSweep,
text = "清除搜索历史"
)
}
}
}
@@ -398,7 +390,7 @@ fun SearchResultItem(
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceContainerLow
LegadoTheme.colorScheme.surfaceContainer
)
) {
Box(modifier = Modifier.padding(16.dp)) {
@@ -408,7 +400,7 @@ fun SearchResultItem(
text = buildAnnotatedString {
append(
result.getTitleSpannable(
MaterialTheme.colorScheme.primary.toArgb()
LegadoTheme.colorScheme.primary.toArgb()
)
)
},
@@ -416,16 +408,18 @@ fun SearchResultItem(
)
Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider()
HorizontalDivider(
color = LegadoTheme.colorScheme.surface
)
Spacer(modifier = Modifier.height(8.dp))
AppText(
text = buildAnnotatedString {
append(
result.getContentSpannable(
textColor = MaterialTheme.colorScheme.onSurface.toArgb(),
accentColor = MaterialTheme.colorScheme.primary.toArgb(),
bgColor = MaterialTheme.colorScheme.primaryContainer.toArgb()
textColor = LegadoTheme.colorScheme.onSurface.toArgb(),
accentColor = LegadoTheme.colorScheme.primary.toArgb(),
bgColor = LegadoTheme.colorScheme.primaryContainer.toArgb()
)
)
},
@@ -441,8 +435,8 @@ fun SearchResultItem(
if (isCurrentChapter) {
TextCard(
text = "当前章节",
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
backgroundColor = LegadoTheme.colorScheme.secondaryContainer,
contentColor = LegadoTheme.colorScheme.onSecondaryContainer,
cornerRadius = 8.dp,
horizontalPadding = 4.dp,
verticalPadding = 2.dp,
@@ -452,8 +446,8 @@ fun SearchResultItem(
if (result.progressPercent > 0f) {
TextCard(
text = String.format("%.1f%%", result.progressPercent),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
backgroundColor = LegadoTheme.colorScheme.secondaryContainer,
contentColor = LegadoTheme.colorScheme.onSecondaryContainer,
cornerRadius = 8.dp,
horizontalPadding = 4.dp,
verticalPadding = 2.dp,
@@ -21,6 +21,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -43,8 +44,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.entities.TxtTocRule
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
@@ -140,16 +143,15 @@ fun TxtRuleScreen(
}
)
if (showUrlInput) {
SourceInputDialog(
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
}
SourceInputDialog(
show = showUrlInput,
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
FilePickerSheet(
show = showExportSheet,
@@ -201,25 +203,19 @@ fun TxtRuleScreen(
}
}
showDeleteRuleDialog?.let { rule ->
AlertDialog(
onDismissRequest = { showDeleteRuleDialog = null },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.del_msg)) },
confirmButton = {
OutlinedButton(onClick = {
viewModel.delete(rule); showDeleteRuleDialog = null
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showDeleteRuleDialog = null }) {
AppText(
stringResource(R.string.cancel)
)
}
}
)
}
AppAlertDialog(
data = showDeleteRuleDialog, // 传入 nullable 的 rule 对象
onDismissRequest = { showDeleteRuleDialog = null },
title = stringResource(R.string.delete),
confirmText = stringResource(R.string.ok),
onConfirm = { rule ->
viewModel.delete(rule)
showDeleteRuleDialog = null
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteRuleDialog = null },
content = { Text(text = stringResource(R.string.del_msg)) }
)
RuleEditSheet(
show = showEditSheet,
@@ -304,8 +300,7 @@ fun TxtRuleScreen(
dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.import_str),
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
onClick = { showImportSheet = true; dismiss() }
)
}
) { paddingValues ->
@@ -316,9 +311,9 @@ fun TxtRuleScreen(
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 8.dp,
bottom = paddingValues.calculateBottomPadding() + 120.dp
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -14,7 +14,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -38,7 +38,7 @@ fun ConfigNavScreen(
title = stringResource(R.string.setting),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -1,28 +1,31 @@
package io.legado.app.ui.config.backupConfig
import android.content.Context
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -37,23 +40,30 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.help.storage.ImportOldData
import io.legado.app.help.storage.Restore
import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.InputSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.takePersistablePermissionSafely
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
import top.yukonga.miuix.kmp.theme.MiuixTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -153,7 +163,7 @@ fun BackupConfigScreen(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let {
io.legado.app.help.storage.ImportOldData.importUri(context, uri)
ImportOldData.importUri(context, uri)
}
}
@@ -169,7 +179,7 @@ fun BackupConfigScreen(
title = stringResource(R.string.backup_restore),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -382,76 +392,69 @@ fun BackupConfigScreen(
}
}
if (showWebDavAuthDialog) {
AlertDialog(
onDismissRequest = { showWebDavAuthDialog = false },
title = { AppText(stringResource(R.string.web_dav_account)) },
text = {
Column {
TextField(
value = tempAccount,
onValueChange = { tempAccount = it },
label = { AppText("账号") }
)
Spacer(modifier = Modifier.padding(8.dp))
TextField(
value = tempPassword,
onValueChange = { tempPassword = it },
label = { AppText("密码") },
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
val image = if (passwordVisible)
Icons.Filled.Visibility
else Icons.Filled.VisibilityOff
val description = if (passwordVisible) "隐藏密码" else "显示密码"
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, description)
}
}
)
}
},
confirmButton = {
TextButton(onClick = {
viewModel.setWebDavAccount(tempAccount, tempPassword)
showWebDavAuthDialog = false
scope.launch {
showLoadingDialog = true
loadingText = "测试中…"
val success = viewModel.testWebDav()
showLoadingDialog = false
if (success) {
snackbarHostState.showSnackbar("WebDav 配置正确")
} else {
snackbarHostState.showSnackbar("WebDav 配置错误")
AppAlertDialog(
show = showWebDavAuthDialog,
onDismissRequest = { showWebDavAuthDialog = false },
title = stringResource(R.string.web_dav_account),
content = {
Column {
AppTextField(
value = tempAccount,
onValueChange = { tempAccount = it },
backgroundColor = LegadoTheme.colorScheme.surface,
label = "账号"
)
Spacer(modifier = Modifier.height(12.dp))
AppTextField(
value = tempPassword,
onValueChange = { tempPassword = it },
backgroundColor = MiuixTheme.colorScheme.surface,
label = "密码",
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
val image =
if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
val description = if (passwordVisible) "隐藏密码" else "显示密码"
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, contentDescription = description)
}
}
}) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { showWebDavAuthDialog = false }) {
AppText(stringResource(R.string.cancel))
)
}
},
confirmText = stringResource(R.string.ok),
onConfirm = {
viewModel.setWebDavAccount(tempAccount, tempPassword)
showWebDavAuthDialog = false
scope.launch {
showLoadingDialog = true
loadingText = "测试中…"
val success = viewModel.testWebDav()
showLoadingDialog = false
if (success) {
snackbarHostState.showSnackbar("WebDav 配置正确")
} else {
snackbarHostState.showSnackbar("WebDav 配置错误")
}
}
)
}
},
dismissText = stringResource(R.string.cancel),
onDismiss = {
showWebDavAuthDialog = false
}
)
if (showConfirmDialog) {
ConfirmDialog(
title = confirmDialogTitle,
text = confirmDialogText,
onConfirm = {
onConfirmAction?.invoke()
showConfirmDialog = false
},
onDismiss = { showConfirmDialog = false }
)
}
ConfirmDialog(
show = showConfirmDialog,
title = confirmDialogTitle,
text = confirmDialogText,
onConfirm = {
onConfirmAction?.invoke()
showConfirmDialog = false
},
onDismiss = { showConfirmDialog = false }
)
FilePickerSheet(
show = showBackupFilePicker,
@@ -478,132 +481,127 @@ fun BackupConfigScreen(
}
)
if (showRestoreSheet && backupNames.isNotEmpty()) {
AlertDialog(
onDismissRequest = { showRestoreSheet = false },
title = { AppText(stringResource(R.string.select_restore_file)) },
text = {
Column {
backupNames.forEach { name ->
TextButton(onClick = {
showRestoreSheet = false
showLoadingDialog = true
loadingText = "恢复中…"
viewModel.restoreWebDav(
name,
{
showLoadingDialog = false
scope.launch {
snackbarHostState.showSnackbar("恢复成功")
}
},
{ error ->
showLoadingDialog = false
scope.launch {
snackbarHostState.showSnackbar("WebDav恢复出错\n$error")
}
AppModalBottomSheet(
show = showRestoreSheet && backupNames.isNotEmpty(),
onDismissRequest = { showRestoreSheet = false },
title = stringResource(R.string.select_restore_file)
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(backupNames) {
SelectionItemCard(
title = it,
containerColor = LegadoTheme.colorScheme.surface,
onToggleSelection = {
showRestoreSheet = false
showLoadingDialog = true
loadingText = "恢复中…"
viewModel.restoreWebDav(
it,
{
showLoadingDialog = false
scope.launch {
snackbarHostState.showSnackbar("恢复成功")
}
)
}) {
AppText(name)
}
},
{ error ->
showLoadingDialog = false
scope.launch {
snackbarHostState.showSnackbar("WebDav恢复出错\n$error")
}
}
)
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = { showRestoreSheet = false }) {
AppText(stringResource(R.string.cancel))
}
}
)
}
if (showBackupIgnoreDialog) {
val checkedItems = remember {
BooleanArray(io.legado.app.help.storage.BackupConfig.ignoreKeys.size) { index ->
io.legado.app.help.storage.BackupConfig.ignoreConfig[
io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
] ?: false
)
}
}
AlertDialog(
onDismissRequest = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
},
title = { AppText(stringResource(R.string.restore_ignore)) },
text = {
Column {
io.legado.app.help.storage.BackupConfig.ignoreTitle.forEachIndexed { index, title ->
TextButton(onClick = {
checkedItems[index] = !checkedItems[index]
io.legado.app.help.storage.BackupConfig.ignoreConfig[
io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
] = checkedItems[index]
}) {
val isChecked = checkedItems[index]
AppText(if (isChecked) "$title" else title)
}
}
}
},
confirmButton = {
TextButton(onClick = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
}) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
}) {
AppText(stringResource(R.string.cancel))
}
}
)
}
if (showLoadingDialog) {
AlertDialog(
onDismissRequest = {},
title = { AppText(loadingText) },
confirmButton = {},
dismissButton = {}
)
val checkedItems = remember(showBackupIgnoreDialog) {
val keys = io.legado.app.help.storage.BackupConfig.ignoreKeys
val config = io.legado.app.help.storage.BackupConfig.ignoreConfig
// 初始化可观察的 List 状态
mutableStateListOf(*Array(keys.size) { index ->
config[keys[index]] ?: false
})
}
AppAlertDialog(
show = showBackupIgnoreDialog,
onDismissRequest = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
},
title = stringResource(R.string.restore_ignore),
content = {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
io.legado.app.help.storage.BackupConfig.ignoreTitle.forEachIndexed { index, title ->
CheckboxItem(
title = title,
checked = checkedItems[index],
onCheckedChange = { isChecked ->
checkedItems[index] = isChecked
io.legado.app.help.storage.BackupConfig.ignoreConfig[
io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
] = isChecked
}
)
}
}
},
confirmText = stringResource(R.string.ok),
onConfirm = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
},
dismissText = stringResource(R.string.cancel),
onDismiss = {
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
showBackupIgnoreDialog = false
}
)
AppAlertDialog(
show = showLoadingDialog,
onDismiss = {},
onConfirm = {},
title = loadingText,
onDismissRequest = { showLoadingDialog = false }
)
}
@Composable
fun ConfirmDialog(
show: Boolean,
title: String,
text: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
AppAlertDialog(
show = show,
onDismissRequest = onDismiss,
title = { AppText(title) },
text = { AppText(text) },
confirmButton = {
OutlinedButton(onClick = onConfirm) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
AppText(stringResource(R.string.cancel))
}
}
title = title,
text = text,
confirmText = stringResource(R.string.ok),
onConfirm = onConfirm,
dismissText = stringResource(R.string.cancel),
onDismiss = onDismiss
)
}
private fun startBackup(
path: String,
context: android.content.Context,
context: Context,
viewModel: BackupConfigViewModel,
onSuccess: () -> Unit,
onError: (String) -> Unit
@@ -27,7 +27,7 @@ import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
@@ -54,7 +54,7 @@ fun CoverConfigScreen(
title = stringResource(R.string.cover_config),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -7,25 +7,19 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -33,15 +27,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.help.DirectLinkUpload
import io.legado.app.lib.dialogs.selector
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.GSON
@@ -67,102 +64,89 @@ fun DirectLinkUploadBottomSheet(
AppModalBottomSheet(
show = show,
title = stringResource(R.string.direct_link_upload_config),
endAction = {
MediumIconButton(
onClick = { showMenu = true },
icon = Icons.Default.MoreVert
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
RoundDropdownMenuItem(
text = "导入默认",
leadingIcon = { Icon(Icons.Default.Download, null) },
onClick = {
showMenu = false
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
viewModel.upView(rule)
}
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.copy_rule),
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
onClick = {
showMenu = false
val rule = DirectLinkUpload.Rule(
viewModel.uploadUrl,
viewModel.downloadUrlRule,
viewModel.summary,
viewModel.compress
)
context.sendToClip(GSON.toJson(rule))
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.paste_rule),
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
showMenu = false
runCatching {
context.getClipText()?.let {
val rule =
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
.getOrThrow()
viewModel.upView(rule)
}
}.onFailure {
context.toastOnUi("剪贴板为空或格式不对")
}
}
)
}
},
onDismissRequest = onDismiss
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp)
.verticalScroll(rememberScrollState())
) {
CenterAlignedTopAppBar(
title = {
AppText(
text = stringResource(R.string.direct_link_upload_config),
style = LegadoTheme.typography.titleLarge
)
},
actions = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { AppText("导入默认") },
leadingIcon = { Icon(Icons.Default.Download, null) },
onClick = {
showMenu = false
context.selector(DirectLinkUpload.defaultRules) { _, rule, _ ->
viewModel.upView(rule)
}
}
)
DropdownMenuItem(
text = { AppText(stringResource(R.string.copy_rule)) },
leadingIcon = { Icon(Icons.Default.ContentCopy, null) },
onClick = {
showMenu = false
val rule = DirectLinkUpload.Rule(
viewModel.uploadUrl,
viewModel.downloadUrlRule,
viewModel.summary,
viewModel.compress
)
context.sendToClip(GSON.toJson(rule))
}
)
DropdownMenuItem(
text = { AppText(stringResource(R.string.paste_rule)) },
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
showMenu = false
runCatching {
context.getClipText()?.let {
val rule =
GSON.fromJsonObject<DirectLinkUpload.Rule>(it)
.getOrThrow()
viewModel.upView(rule)
}
}.onFailure {
context.toastOnUi("剪贴板为空或格式不对")
}
}
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
)
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
AppTextField(
value = viewModel.uploadUrl,
onValueChange = { viewModel.uploadUrl = it },
label = { AppText(stringResource(R.string.upload_url)) },
label = stringResource(R.string.upload_url),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
AppTextField(
value = viewModel.downloadUrlRule,
onValueChange = { viewModel.downloadUrlRule = it },
label = { AppText(stringResource(R.string.download_url_rule)) },
label = stringResource(R.string.download_url_rule),
modifier = Modifier.fillMaxWidth(),
)
Spacer(Modifier.height(16.dp))
OutlinedTextField(
AppTextField(
value = viewModel.summary,
onValueChange = { viewModel.summary = it },
label = { AppText(stringResource(R.string.summary)) },
label = stringResource(R.string.summary),
modifier = Modifier.fillMaxWidth(),
)
@@ -200,19 +184,22 @@ fun DirectLinkUploadBottomSheet(
}
}
showTestResult?.let { result ->
AlertDialog(
onDismissRequest = { showTestResult = null },
title = { AppText("Result") },
text = { AppText(result) },
confirmButton = {
TextButton(onClick = {
showTestResult = null
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { context.sendToClip(result) }) { AppText(stringResource(R.string.copy_text)) }
AppAlertDialog(
data = showTestResult,
onDismissRequest = { showTestResult = null },
title = "Result",
content = { result ->
SelectionContainer {
AppText(text = result)
}
)
}
},
confirmText = stringResource(R.string.ok),
onConfirm = {
showTestResult = null
},
dismissText = stringResource(R.string.copy_text),
onDismiss = {
showTestResult?.let { context.sendToClip(it) }
}
)
}
@@ -7,14 +7,11 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -28,16 +25,18 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.service.WebService
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
import io.legado.app.ui.widget.components.settingItem.InputSettingItem
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.utils.restart
@@ -86,7 +85,7 @@ fun OtherConfigScreen(
title = stringResource(R.string.other_setting),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -423,90 +422,63 @@ fun OtherConfigScreen(
onDismiss = { showDirectLinkUploadSheet = false }
)
if (showClearCacheDialog) {
ConfirmDialog(
title = stringResource(R.string.clear_cache),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearCache(context)
showClearCacheDialog = false
},
onDismiss = { showClearCacheDialog = false }
)
}
AppAlertDialog(
show = showClearCacheDialog,
onDismissRequest = { showClearCacheDialog = false },
title = stringResource(R.string.clear_cache),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearCache(context)
showClearCacheDialog = false
},
onDismiss = { showClearCacheDialog = false }
)
if (showShrinkDbDialog) {
ConfirmDialog(
title = stringResource(R.string.shrink_database),
text = stringResource(R.string.sure),
onConfirm = {
viewModel.shrinkDatabase()
showShrinkDbDialog = false
},
onDismiss = { showShrinkDbDialog = false }
)
}
AppAlertDialog(
show = showShrinkDbDialog,
onDismissRequest = { showShrinkDbDialog = false },
title = stringResource(R.string.shrink_database),
text = stringResource(R.string.sure),
onConfirm = {
viewModel.shrinkDatabase()
showShrinkDbDialog = false
},
onDismiss = { showShrinkDbDialog = false }
)
if (showClearWebViewDialog) {
ConfirmDialog(
title = stringResource(R.string.clear_webview_data),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearWebViewData(context)
showClearWebViewDialog = false
},
onDismiss = { showClearWebViewDialog = false }
)
}
AppAlertDialog(
show = showClearWebViewDialog,
onDismissRequest = { showClearWebViewDialog = false },
title = stringResource(R.string.clear_webview_data),
text = stringResource(R.string.sure_del),
onConfirm = {
viewModel.clearWebViewData(context)
showClearWebViewDialog = false
},
onDismiss = { showClearWebViewDialog = false }
)
if (showPasswordDialog) {
AlertDialog(
onDismissRequest = { showPasswordDialog = false },
title = { AppText(stringResource(R.string.set_local_password)) },
text = {
TextField(
value = tempPassword,
onValueChange = { tempPassword = it },
label = { AppText("Password") }
)
},
confirmButton = {
TextButton(onClick = {
viewModel.setLocalPassword(tempPassword)
showPasswordDialog = false
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showPasswordDialog = false }) {
AppText(stringResource(R.string.cancel))
}
}
)
}
AppAlertDialog(
show = showPasswordDialog,
onDismissRequest = { showPasswordDialog = false },
title = stringResource(R.string.set_local_password),
content = {
AppTextField(
value = tempPassword,
onValueChange = { tempPassword = it },
label = "Password",
backgroundColor = LegadoTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth()
)
},
confirmText = stringResource(R.string.ok),
onConfirm = {
viewModel.setLocalPassword(tempPassword)
showPasswordDialog = false
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showPasswordDialog = false }
)
}
}
@Composable
fun ConfirmDialog(
title: String,
text: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { AppText(title) },
text = { AppText(text) },
confirmButton = {
OutlinedButton(onClick = onConfirm) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
AppText(stringResource(R.string.cancel))
}
}
)
}
@@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
@@ -47,7 +47,7 @@ fun ReadConfigScreen(
title = stringResource(R.string.read_config),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -83,8 +83,10 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeManager
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
@@ -131,7 +133,7 @@ fun ThemeConfigScreen(
title = stringResource(R.string.theme_setting),
scrollBehavior = scrollBehavior,
navigationIcon = {
TopbarNavigationButton(onClick = onBackClick)
TopBarNavigationButton(onClick = onBackClick)
}
)
}
@@ -527,30 +529,25 @@ fun ThemeConfigScreen(
}
}
if (showRestartDialog) {
AlertDialog(
onDismissRequest = { showRestartDialog = false },
title = { AppText(stringResource(R.string.restart_required_message)) },
confirmButton = {
OutlinedButton(onClick = {
showRestartDialog = false
Handler(Looper.getMainLooper()).postDelayed({
context.restart()
}, 100)
}) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = {
showRestartDialog = false
context.toastOnUi(R.string.restart_later_message)
}) {
AppText(stringResource(R.string.cancel))
}
}
)
}
AppAlertDialog(
show = showRestartDialog,
onDismissRequest = { showRestartDialog = false },
title = stringResource(R.string.restart_required_message),
onConfirm = {
showRestartDialog = false
Handler(Looper.getMainLooper()).postDelayed({
context.restart()
}, 100)
},
confirmText = stringResource(R.string.ok),
onDismiss = {
showRestartDialog = false
context.toastOnUi(R.string.restart_later_message)
},
dismissText = stringResource(R.string.cancel)
)
manageKey?.let { isDark ->
BackgroundImageManageSheet(
@@ -595,38 +592,31 @@ fun ThemeConfigScreen(
onDismissRequest = { showThemeListDialog = false }
)
saveThemeKey?.let { key ->
AlertDialog(
onDismissRequest = { saveThemeKey = null },
title = { AppText(stringResource(R.string.theme_name)) },
text = {
OutlinedTextField(
value = themeName,
onValueChange = { themeName = it },
label = { AppText("name") },
singleLine = true
)
},
confirmButton = {
TextButton(
onClick = {
when (key) {
"saveDayTheme" -> OldThemeConfig.saveDayTheme(context, themeName)
"saveNightTheme" -> OldThemeConfig.saveNightTheme(context, themeName)
}
saveThemeKey = null
}
) {
AppText(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { saveThemeKey = null }) {
AppText(stringResource(android.R.string.cancel))
}
AppAlertDialog(
data = saveThemeKey,
onDismissRequest = { saveThemeKey = null },
title = stringResource(R.string.theme_name),
content = {
AppTextField(
value = themeName,
onValueChange = { themeName = it },
label = "name",
singleLine = true,
modifier = Modifier.fillMaxWidth(),
backgroundColor = LegadoTheme.colorScheme.surface
)
},
confirmText = stringResource(android.R.string.ok),
onConfirm = { key ->
when (key) {
"saveDayTheme" -> OldThemeConfig.saveDayTheme(context, themeName)
"saveNightTheme" -> OldThemeConfig.saveNightTheme(context, themeName)
}
)
}
saveThemeKey = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { saveThemeKey = null }
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -37,6 +37,7 @@ import io.legado.app.R
import io.legado.app.help.config.OldThemeConfig
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
@@ -158,27 +159,17 @@ fun ThemeListDialog(
}
}
deleteIndex?.let { index ->
AlertDialog(
onDismissRequest = { deleteIndex = null },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.sure_del)) },
confirmButton = {
TextButton(
onClick = {
OldThemeConfig.delConfig(index)
listVersion++
deleteIndex = null
}
) {
AppText(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { deleteIndex = null }) {
AppText(stringResource(android.R.string.cancel))
}
}
)
}
AppAlertDialog(
data = deleteIndex,
onDismissRequest = { deleteIndex = null },
title = stringResource(R.string.delete),
confirmText = stringResource(android.R.string.ok),
onConfirm = { index ->
OldThemeConfig.delConfig(index)
listVersion++
deleteIndex = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { deleteIndex = null }
)
}
@@ -17,10 +17,8 @@ import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -36,6 +34,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.DictRule
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch
@@ -152,7 +152,7 @@ fun DictRuleEditSheet(
}
}
FloatingActionButton(
AppFloatingActionButton(
onClick = {
onSave(
rule?.copy(name = name, urlRule = urlRule, showRule = showRule)
@@ -161,11 +161,9 @@ fun DictRuleEditSheet(
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
.padding(16.dp)
) {
Icon(Icons.Default.Save, contentDescription = "Save")
AppIcon(Icons.Default.Save, contentDescription = "Save")
}
}
}
@@ -5,7 +5,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
@@ -13,15 +12,10 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -43,8 +37,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.entities.DictRule
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
@@ -55,7 +51,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.rules.RuleEditFields
import io.legado.app.ui.widget.components.rules.RuleEditSheet
import io.legado.app.ui.widget.components.rules.RuleListScaffold
import io.legado.app.ui.widget.components.text.AppText
import org.koin.androidx.compose.koinViewModel
import sh.calvin.reorderable.rememberReorderableLazyListState
@@ -139,16 +134,15 @@ fun DictRuleScreen(
}
)
if (showUrlInput) {
SourceInputDialog(
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
}
SourceInputDialog(
show = showUrlInput,
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
FilePickerSheet(
show = showExportSheet,
@@ -200,25 +194,18 @@ fun DictRuleScreen(
}
}
showDeleteRuleDialog?.let { rule ->
AlertDialog(
onDismissRequest = { showDeleteRuleDialog = null },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.del_msg)) },
confirmButton = {
OutlinedButton(onClick = {
viewModel.delete(rule); showDeleteRuleDialog = null
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showDeleteRuleDialog = null }) {
AppText(
stringResource(R.string.cancel)
)
}
}
)
}
AppAlertDialog(
data = showDeleteRuleDialog,
onDismissRequest = { showDeleteRuleDialog = null },
title = stringResource(R.string.delete),
confirmText = stringResource(R.string.ok),
onConfirm = { rule ->
viewModel.delete(rule)
showDeleteRuleDialog = null
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteRuleDialog = null }
)
RuleEditSheet(
show = showEditSheet,
@@ -302,8 +289,7 @@ fun DictRuleScreen(
dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.import_str),
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
onClick = { showImportSheet = true; dismiss() }
)
}
) { paddingValues ->
@@ -314,9 +300,9 @@ fun DictRuleScreen(
FastScrollLazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 8.dp,
bottom = paddingValues.calculateBottomPadding() + 120.dp
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -36,7 +36,6 @@ import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.cover.BookCover
import io.legado.app.ui.widget.components.cover.BookshelfCover
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.toTimeAgo
@@ -208,10 +207,12 @@ fun BookGroupCover(
modifier = modifier
.aspectRatio(5f / 7f)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.background(LegadoTheme.colorScheme.surfaceContainer)
) {
if (!coverPath.isNullOrBlank()) {
Cover(
BookCover(
name = null,
author = null,
path = coverPath,
modifier = Modifier.fillMaxSize()
)
@@ -339,8 +340,6 @@ fun BookGroupItemList(
titleMaxLines = titleMaxLines,
coverShadow = coverShadow,
modifier = modifier,
subTitle = "${previewBooks.size} 本书籍",
desc = "点击打开文件夹",
onClick = onClick,
onLongClick = onLongClick
)
@@ -26,7 +26,6 @@ import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.ImportExport
@@ -74,6 +73,7 @@ import io.legado.app.ui.book.manage.BookshelfManageActivity
import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
@@ -299,8 +299,7 @@ fun BookshelfScreen(
)
RoundDropdownMenuItem(
text = stringResource(R.string.import_bookshelf),
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
onClick = { showImportSheet = true; dismiss() }
)
RoundDropdownMenuItem(
text = stringResource(R.string.log),
@@ -404,11 +403,9 @@ fun BookshelfScreen(
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp,
start = if (bookshelfLayoutMode != 0) 12.dp else 0.dp,
end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
bottom = 120.dp
),
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
@@ -499,16 +496,15 @@ fun BookshelfScreen(
onDismissRequest = { showGroupManageSheet = false }
)
if (showAddUrlDialog) {
SourceInputDialog(
title = stringResource(R.string.add_book_url),
onDismissRequest = { showAddUrlDialog = false },
onConfirm = { url ->
viewModel.addBookByUrl(url)
showAddUrlDialog = false
}
)
}
SourceInputDialog(
show = showAddUrlDialog,
title = stringResource(R.string.add_book_url),
onDismissRequest = { showAddUrlDialog = false },
onConfirm = { url ->
viewModel.addBookByUrl(url)
showAddUrlDialog = false
}
)
FilePickerSheet(
show = showImportSheet,
@@ -583,11 +579,10 @@ fun BookshelfPage(
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(columns.coerceAtLeast(1)),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp,
start = if (bookshelfLayoutMode != 0) 12.dp else 0.dp,
end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
horizontal = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
),
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
@@ -1,19 +1,13 @@
package io.legado.app.ui.main.explore
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
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.fillMaxSize
@@ -32,14 +26,12 @@ import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.VerticalAlignTop
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -67,12 +59,15 @@ import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
import io.legado.app.ui.widget.components.explore.ExploreKindItem
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
import io.legado.app.ui.widget.components.list.ListScaffold
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -163,7 +158,7 @@ fun ExploreScreen(
FastScrollLazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
)
@@ -211,7 +206,7 @@ fun ExploreScreen(
modifier = Modifier
.fillMaxWidth()
.animateItem()
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
rowItems.forEach { (kind, span) ->
@@ -245,54 +240,42 @@ fun ExploreScreen(
}
}
AnimatedVisibility(
visible = stickyHeaderSource != null,
TopFloatingStickyItem(
item = stickyHeaderSource,
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp),
enter = fadeIn() + slideInVertically { -it },
exit = fadeOut() + slideOutVertically { -it }
) {
var lastSource by remember { mutableStateOf<BookSourcePart?>(null) }
stickyHeaderSource?.let { lastSource = it }
lastSource?.let { item ->
ExploreStickyCard(
item = item,
onClick = {
scope.launch {
val index =
uiState.items.indexOfFirst { it.bookSourceUrl == item.bookSourceUrl }
if (index >= 0) listState.animateScrollToItem(index)
}
},
isMiuix = composeEngine
)
}
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp)
) { item ->
TextCard(
text = item.bookSourceName,
textStyle = LegadoTheme.typography.labelLarge,
horizontalPadding = 8.dp,
verticalPadding = 6.dp,
onClick = {
scope.launch {
val index =
uiState.items.indexOfFirst { it.bookSourceUrl == item.bookSourceUrl }
if (index >= 0) listState.animateScrollToItem(index)
}
}
)
}
}
}
sourceToDelete?.let { source ->
AlertDialog(
onDismissRequest = { sourceToDelete = null },
title = { AppText(stringResource(R.string.draw)) },
text = { AppText(stringResource(R.string.sure_del) + "\n" + source.bookSourceName) },
confirmButton = {
TextButton(onClick = {
viewModel.deleteSource(source)
sourceToDelete = null
}) {
AppText(stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(onClick = { sourceToDelete = null }) {
AppText(stringResource(R.string.no))
}
}
)
}
AppAlertDialog(
data = sourceToDelete,
onDismissRequest = { sourceToDelete = null },
title = stringResource(R.string.sure_del),
confirmText = stringResource(android.R.string.ok),
onConfirm = { source ->
viewModel.deleteSource(source)
sourceToDelete = null
},
dismissText = stringResource(android.R.string.cancel),
onDismiss = { sourceToDelete = null },
)
}
private fun calculateRows(
@@ -369,7 +352,7 @@ fun ExploreSourceHeader(
GlassCard(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
.padding(vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
containerColor = containerColor
) {
@@ -452,18 +435,4 @@ fun ExploreSourceHeader(
}
}
@Composable
fun ExploreStickyCard(
item: BookSourcePart,
onClick: () -> Unit,
isMiuix: Boolean,
) {
TextCard(
text = item.bookSourceName,
textStyle = LegadoTheme.typography.labelLarge,
horizontalPadding = 8.dp,
verticalPadding = 6.dp,
onClick = onClick
)
}
@@ -10,13 +10,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.automirrored.filled.HelpOutline
@@ -52,7 +50,7 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
import io.legado.app.ui.dict.rule.DictRuleActivity
import io.legado.app.ui.file.FileManageActivity
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.SmallTextButton
@@ -91,120 +89,113 @@ fun MyScreen(
)
}
) { padding ->
Column(
modifier = Modifier
.verticalScroll(rememberScrollState()),
//TODO:暂时这样解决,改天加模糊
) {
Spacer(
modifier = Modifier
.padding(
top = padding.calculateTopPadding()
)
LazyColumn(
modifier = Modifier,
contentPadding = adaptiveContentPadding(
top = padding.calculateTopPadding(),
bottom = 120.dp
)
) {
item {
SplicedColumnGroup(
title = ""
) {
WebServiceSettingBlock(
uiState = uiState,
viewModel = viewModel,
onNavigate = onNavigate
)
}
SplicedColumnGroup(
modifier = Modifier.adaptiveHorizontalPadding(),
title = ""
) {
WebServiceSettingBlock(
uiState = uiState,
viewModel = viewModel,
onNavigate = onNavigate
)
SplicedColumnGroup(
title = "规则"
) {
ClickableSettingItem(
title = stringResource(R.string.book_source_manage),
description = stringResource(R.string.book_source_manage_desc),
imageVector = Icons.Default.Source,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(BookSourceActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.replace_purify),
imageVector = Icons.Default.FindReplace,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(ReplaceRuleActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.txt_toc_rule),
imageVector = Icons.AutoMirrored.Filled.Rule,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(TxtTocRuleActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.dict_rule),
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(DictRuleActivity::class.java)
)
}
)
}
SplicedColumnGroup(
title = stringResource(R.string.other)
) {
ClickableSettingItem(
title = stringResource(R.string.setting),
imageVector = Icons.Default.Settings,
onClick = {
onOpenSettings()
}
)
ClickableSettingItem(
title = stringResource(R.string.bookmark),
imageVector = Icons.Default.Bookmark,
onClick = {
onNavigate(PrefClickEvent.StartActivity(AllBookmarkActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.read_record),
imageVector = Icons.Default.History,
onClick = {
onNavigate(PrefClickEvent.StartActivity(ReadRecordActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.file_manage),
imageVector = Icons.Default.Folder,
onClick = {
onNavigate(PrefClickEvent.StartActivity(FileManageActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.about),
imageVector = Icons.Default.Info,
onClick = {
onNavigate(PrefClickEvent.StartActivity(AboutActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.exit),
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
onClick = {
onNavigate(PrefClickEvent.ExitApp)
}
)
}
}
SplicedColumnGroup(
modifier = Modifier.adaptiveHorizontalPadding(),
title = "规则"
) {
ClickableSettingItem(
title = stringResource(R.string.book_source_manage),
description = stringResource(R.string.book_source_manage_desc),
imageVector = Icons.Default.Source,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(BookSourceActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.replace_purify),
imageVector = Icons.Default.FindReplace,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(ReplaceRuleActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.txt_toc_rule),
imageVector = Icons.AutoMirrored.Filled.Rule,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(TxtTocRuleActivity::class.java)
)
}
)
ClickableSettingItem(
title = stringResource(R.string.dict_rule),
imageVector = Icons.AutoMirrored.Filled.LibraryBooks,
onClick = {
onNavigate(
PrefClickEvent.StartActivity(DictRuleActivity::class.java)
)
}
)
}
SplicedColumnGroup(
modifier = Modifier.adaptiveHorizontalPadding(),
title = stringResource(R.string.other)) {
ClickableSettingItem(
title = stringResource(R.string.setting),
imageVector = Icons.Default.Settings,
onClick = {
onOpenSettings()
}
)
ClickableSettingItem(
title = stringResource(R.string.bookmark),
imageVector = Icons.Default.Bookmark,
onClick = {
onNavigate(PrefClickEvent.StartActivity(AllBookmarkActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.read_record),
imageVector = Icons.Default.History,
onClick = {
onNavigate(PrefClickEvent.StartActivity(ReadRecordActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.file_manage),
imageVector = Icons.Default.Folder,
onClick = {
onNavigate(PrefClickEvent.StartActivity(FileManageActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.about),
imageVector = Icons.Default.Info,
onClick = {
onNavigate(PrefClickEvent.StartActivity(AboutActivity::class.java))
}
)
ClickableSettingItem(
title = stringResource(R.string.exit),
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
onClick = {
onNavigate(PrefClickEvent.ExitApp)
}
)
}
Spacer(Modifier.height(120.dp))
}
}
}
@@ -55,7 +55,9 @@ import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
import io.legado.app.ui.rss.source.manage.RssSourceActivity
import io.legado.app.ui.rss.subscription.RuleSubActivity
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.SourceIcon
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.divider.PillDivider
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
@@ -135,12 +137,10 @@ fun RssScreen(
context.startActivity<RssSourceActivity>()
dismiss()
},
leadingIcon = { MenuItemIcon(Icons.Default.Settings) },
text = "订阅源管理"
)
PillDivider()
RoundDropdownMenuItem(
leadingIcon = { MenuItemIcon(Icons.Default.Group) },
text = stringResource(R.string.all),
onClick = {
viewModel.setGroup("")
@@ -149,7 +149,6 @@ fun RssScreen(
)
uiState.groups.forEach { group ->
RoundDropdownMenuItem(
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Outlined.Label) },
text = group,
onClick = {
viewModel.setGroup(group)
@@ -162,10 +161,8 @@ fun RssScreen(
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 72.dp),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = 12.dp,
end = 12.dp,
top = paddingValues.calculateTopPadding() + 8.dp,
contentPadding = adaptiveContentPadding(
top = paddingValues.calculateTopPadding(),
bottom = 120.dp
),
horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -186,28 +183,18 @@ fun RssScreen(
}
}
sourceToDelete?.let { source ->
AlertDialog(
onDismissRequest = { sourceToDelete = null },
title = { AppText(stringResource(R.string.draw)) },
text = { AppText(stringResource(R.string.sure_del) + "\n" + source.sourceName) },
confirmButton = {
TextButton(
onClick = {
viewModel.del(source)
sourceToDelete = null
}
) {
AppText(stringResource(R.string.yes))
}
},
dismissButton = {
TextButton(onClick = { sourceToDelete = null }) {
AppText(stringResource(R.string.no))
}
}
)
}
AppAlertDialog(
data = sourceToDelete,
onDismissRequest = { sourceToDelete = null },
title = stringResource(R.string.draw),
confirmText = stringResource(R.string.yes),
onConfirm = { source ->
viewModel.del(source)
sourceToDelete = null
},
dismissText = stringResource(R.string.no),
onDismiss = { sourceToDelete = null }
)
}
@OptIn(ExperimentalFoundationApi::class)
@@ -1,9 +1,10 @@
package io.legado.app.ui.replace
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable
object ReplaceRuleRoute
object ReplaceRuleRoute : NavKey
@Serializable
data class ReplaceEditRoute(
@@ -13,4 +14,4 @@ data class ReplaceEditRoute(
val scope: String? = null,
val isScopeTitle: Boolean = false,
val isScopeContent: Boolean = false
)
) : NavKey
@@ -1,16 +1,23 @@
package io.legado.app.ui.replace
package io.legado.app.ui.replace
import android.content.Context
import android.content.Intent
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import io.legado.app.base.BaseComposeActivity
import io.legado.app.ui.replace.edit.ReplaceEditScreen
import io.legado.app.ui.replace.edit.ReplaceEditViewModel
@@ -36,59 +43,132 @@ class ReplaceRuleActivity : BaseComposeActivity() {
@Composable
override fun Content() {
AppTheme {
val navController = rememberNavController()
val context = LocalActivity.current
val startRouteJson = intent.getStringExtra(EXTRA_START_ROUTE)
val backStack = rememberNavBackStack(
remember(startRouteJson) {
resolveStartRoute(startRouteJson)
}
)
LaunchedEffect(Unit) {
context?.setResult(RESULT_OK)
}
val startRouteJson = intent.getStringExtra(EXTRA_START_ROUTE)
val initialEditRoute = remember(startRouteJson) {
startRouteJson?.let { Json.decodeFromString<ReplaceEditRoute>(it) }
}
val startDestination: Any = initialEditRoute ?: ReplaceRuleRoute
SharedTransitionLayout {
NavHost(
navController = navController,
startDestination = startDestination
) {
composable<ReplaceRuleRoute> {
NavDisplay(
backStack = backStack,
transitionSpec = {
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 480,
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> fullWidth }
) + fadeIn(
animationSpec = tween(
durationMillis = 360,
easing = LinearOutSlowInEasing
)
)) togetherWith (slideOutOfContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 480,
easing = FastOutSlowInEasing
),
targetOffset = { fullWidth -> fullWidth / 4 }
) + fadeOut(
animationSpec = tween(
durationMillis = 360,
easing = LinearOutSlowInEasing
)
))
},
popTransitionSpec = {
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
durationMillis = 480,
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> -fullWidth / 4 }
) + fadeIn(
animationSpec = tween(
durationMillis = 360,
easing = LinearOutSlowInEasing
)
)) togetherWith (scaleOut(
targetScale = 0.8f,
animationSpec = tween(
durationMillis = 480,
easing = FastOutSlowInEasing
)
) + fadeOut(
animationSpec = tween(durationMillis = 360)
))
},
predictivePopTransitionSpec = { _ ->
(slideIntoContainer(
towards = AnimatedContentTransitionScope.SlideDirection.Start,
animationSpec = tween(
easing = FastOutSlowInEasing
),
initialOffset = { fullWidth -> -fullWidth / 4 }
) + fadeIn(
animationSpec = tween(
easing = LinearOutSlowInEasing
)
)) togetherWith (scaleOut(
targetScale = 0.8f,
animationSpec = tween(
easing = FastOutSlowInEasing
)
) + fadeOut(
animationSpec = tween()
))
},
onBack = {
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
finish()
}
},
entryProvider = entryProvider {
entry<ReplaceRuleRoute> {
ReplaceRuleScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable,
onBackClick = { finish() },
onNavigateToEdit = { route -> navController.navigate(route) }
onNavigateToEdit = { route -> backStack.add(route) }
)
}
composable<ReplaceEditRoute> { backStackEntry ->
val route = backStackEntry.toRoute<ReplaceEditRoute>()
entry<ReplaceEditRoute> { route ->
val viewModel: ReplaceEditViewModel = koinViewModel { parametersOf(route) }
ReplaceEditScreen(
sharedTransitionScope = this@SharedTransitionLayout,
animatedVisibilityScope = this@composable,
viewModel = viewModel,
onBack = {
// 如果编辑页是唯一的页面,点击返回应关闭 Activity
if (navController.previousBackStackEntry == null) {
finish()
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
navController.popBackStack()
finish()
}
},
onSaveSuccess = {
if (navController.previousBackStackEntry == null) finish()
else navController.popBackStack()
if (backStack.size > 1) {
backStack.removeLastOrNull()
} else {
finish()
}
}
)
}
}
}
)
}
}
}
private fun resolveStartRoute(route: String?): NavKey {
return route?.let { Json.decodeFromString<ReplaceEditRoute>(it) } ?: ReplaceRuleRoute
}
}
@@ -3,14 +3,13 @@ package io.legado.app.ui.replace
import android.content.ClipData
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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
@@ -20,19 +19,14 @@ import androidx.compose.material.icons.filled.FileOpen
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -55,12 +49,16 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.GroupManageBottomSheet
import io.legado.app.ui.widget.components.alert.AppAlertDialog
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.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
@@ -78,8 +76,6 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
)
@Composable
fun ReplaceRuleScreen(
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
viewModel: ReplaceRuleViewModel = koinViewModel(),
onBackClick: () -> Unit,
onNavigateToEdit: (ReplaceEditRoute) -> Unit,
@@ -147,16 +143,15 @@ fun ReplaceRuleScreen(
}
)
if (showUrlInput) {
SourceInputDialog(
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
}
SourceInputDialog(
show = showUrlInput,
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
FilePickerSheet(
show = showImportSheet,
@@ -262,25 +257,18 @@ fun ReplaceRuleScreen(
)
showDeleteRuleDialog?.let { rule ->
AlertDialog(
onDismissRequest = { showDeleteRuleDialog = null },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.del_msg)) },
confirmButton = {
OutlinedButton(onClick = {
viewModel.delete(rule); showDeleteRuleDialog = null
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showDeleteRuleDialog = null }) {
AppText(
stringResource(R.string.cancel)
)
}
}
)
}
AppAlertDialog(
data = showDeleteRuleDialog,
onDismissRequest = { showDeleteRuleDialog = null },
title = stringResource(R.string.delete),
confirmText = stringResource(R.string.ok),
onConfirm = { rule ->
viewModel.delete(rule)
showDeleteRuleDialog = null
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteRuleDialog = null }
)
RuleListScaffold(
title = "替换规则",
@@ -334,6 +322,8 @@ fun ReplaceRuleScreen(
bottomContent = {
if (tabItems.size > 1) {
AppTabRow(
modifier = Modifier
.adaptiveHorizontalPadding(),
tabTitles = tabItems,
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
@@ -344,39 +334,24 @@ fun ReplaceRuleScreen(
}
},
floatingActionButton = {
TooltipBox(
positionProvider =
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = { PlainTooltip { AppText("Localized description") } },
state = rememberTooltipState(),
AppFloatingActionButton(
modifier = Modifier.animateFloatingActionButton(
visible = !inSelectionMode,
alignment = Alignment.BottomEnd,
),
onClick = {
onNavigateToEdit(ReplaceEditRoute(id = -1))
},
tooltipText = "Localized description"
) {
with(sharedTransitionScope) {
FloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = !inSelectionMode,
alignment = Alignment.BottomEnd,
)
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "fab_add"),
animatedVisibilityScope = animatedVisibilityScope,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
),
onClick = {
onNavigateToEdit(ReplaceEditRoute(id = -1))
}
) {
Icon(Icons.Default.Add, contentDescription = "Add Rule")
}
}
AppIcon(Icons.Default.Add, contentDescription = "Add Rule")
}
},
snackbarHostState = snackbarHostState,
dropDownMenuContent = { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.import_str),
onClick = { showImportSheet = true; dismiss() },
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
onClick = { showImportSheet = true; dismiss() }
)
RoundDropdownMenuItem(
text = "分组管理",
@@ -401,7 +376,7 @@ fun ReplaceRuleScreen(
viewModel.setSortMode("name_asc")
dismiss()
scope.launch {
snackbarHostState.showSnackbar("非时间排序模式下禁用拖动")
snackbarHostState.showSnackbar("当前排序模式下禁用拖动")
}
}
)
@@ -411,7 +386,7 @@ fun ReplaceRuleScreen(
viewModel.setSortMode("name_desc")
dismiss()
scope.launch {
snackbarHostState.showSnackbar("非时间排序模式下禁用拖动")
snackbarHostState.showSnackbar("当前排序模式下禁用拖动")
}
}
)
@@ -425,59 +400,51 @@ fun ReplaceRuleScreen(
state = listState,
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = padding.calculateTopPadding() + 8.dp,
bottom = padding.calculateBottomPadding() + 120.dp
contentPadding = adaptiveContentPadding(
top = padding.calculateTopPadding(),
bottom = 120.dp
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(filteredRules, key = { it.id }) { ui ->
with(sharedTransitionScope) {
ReorderableSelectionItem(
state = reorderableState,
key = ui.id,
title = ui.name,
isEnabled = ui.isEnabled,
isSelected = selectedIds.contains(ui.id),
inSelectionMode = inSelectionMode,
canReorder = canReorder,
onToggleSelection = {
viewModel.toggleSelection(ui.id)
},
onEnabledChange = { enabled ->
viewModel.update(ui.rule.copy(isEnabled = enabled))
},
onClickEdit = {
onNavigateToEdit(
ReplaceEditRoute(
id = ui.id,
pattern = ui.rule.pattern
)
ReorderableSelectionItem(
state = reorderableState,
key = ui.id,
title = ui.name,
isEnabled = ui.isEnabled,
isSelected = selectedIds.contains(ui.id),
inSelectionMode = inSelectionMode,
canReorder = canReorder,
onToggleSelection = {
viewModel.toggleSelection(ui.id)
},
onEnabledChange = { enabled ->
viewModel.update(ui.rule.copy(isEnabled = enabled))
},
onClickEdit = {
onNavigateToEdit(
ReplaceEditRoute(
id = ui.id,
pattern = ui.rule.pattern
)
},
modifier = Modifier
.sharedBounds(
sharedContentState = rememberSharedContentState(key = "rule_${ui.id}"),
animatedVisibilityScope = animatedVisibilityScope,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
//clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(12.dp))
),
dropdownContent = { dismiss ->
RoundDropdownMenuItem(
text = "移至顶部",
onClick = { viewModel.toTop(ui.rule); dismiss() }
)
RoundDropdownMenuItem(
text = "移至底部",
onClick = { viewModel.toBottom(ui.rule); dismiss() }
)
RoundDropdownMenuItem(
text = "删除",
onClick = { showDeleteRuleDialog = ui.rule; dismiss() }
)
}
)
}
)
},
modifier = Modifier,
dropdownContent = { dismiss ->
RoundDropdownMenuItem(
text = "移至顶部",
onClick = { viewModel.toTop(ui.rule); dismiss() }
)
RoundDropdownMenuItem(
text = "移至底部",
onClick = { viewModel.toBottom(ui.rule); dismiss() }
)
RoundDropdownMenuItem(
text = "删除",
onClick = { showDeleteRuleDialog = ui.rule; dismiss() }
)
}
)
}
}
if (inSelectionMode) {
@@ -496,3 +463,4 @@ fun ReplaceRuleScreen(
}
}
}
@@ -1,8 +1,6 @@
package io.legado.app.ui.replace.edit
package io.legado.app.ui.replace.edit
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
@@ -46,18 +44,13 @@ import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.FloatingActionButton
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
@@ -77,8 +70,13 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.AlertButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
@@ -93,272 +91,252 @@ fun keyboardAsState(): State<Boolean> {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ReplaceEditScreen(
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope,
onBack: () -> Unit,
onSaveSuccess: () -> Unit,
viewModel: ReplaceEditViewModel = koinViewModel()
) {
val state by viewModel.uiState.collectAsState()
val sharedKey = remember(state.id) {
if (state.id > 0) "rule_${state.id}" else "fab_add"
}
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
var showMenu by remember { mutableStateOf(false) }
val isKeyboardVisible by keyboardAsState()
with(sharedTransitionScope) {
AppScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
.sharedBounds(
sharedContentState = rememberSharedContentState(key = sharedKey),
animatedVisibilityScope = animatedVisibilityScope,
resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
)
.imePadding(),
topBar = {
GlassMediumFlexibleTopAppBar(
title = if (state.id > 0) "编辑替换规则" else "新增替换规则",
navigationIcon = {
TopbarNavigationButton(onClick = onBack)
},
actions = {
AnimatedVisibility(
visible = isKeyboardVisible,
enter = fadeIn(),
exit = fadeOut()
) {
IconButton(onClick = {
viewModel.save(onSaveSuccess)
}) {
Icon(Icons.Default.Save, contentDescription = "保存")
}
}
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "更多操作")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { AppText("复制规则") },
onClick = {
showMenu = false
viewModel.copyRule()
}
)
DropdownMenuItem(
text = { AppText("粘贴规则") },
onClick = {
showMenu = false
viewModel.pasteRule(onSuccess = {})
}
)
}
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
TooltipBox(
modifier = Modifier.navigationBarsPadding(),
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
TooltipAnchorPosition.Above
),
tooltip = { PlainTooltip { AppText("添加") } },
state = rememberTooltipState(),
) {
FloatingActionButton(
modifier = Modifier.animateFloatingActionButton(
visible = !isKeyboardVisible,
alignment = Alignment.BottomEnd,
),
onClick = { viewModel.save(onSaveSuccess) }
AppScaffold(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = if (state.id > 0) "编辑替换规则" else "新增替换规则",
navigationIcon = {
TopBarNavigationButton(onClick = onBack)
},
actions = {
AnimatedVisibility(
visible = isKeyboardVisible,
enter = fadeIn(),
exit = fadeOut()
) {
Icon(Icons.Default.Save, contentDescription = "保存")
IconButton(onClick = {
viewModel.save(onSaveSuccess)
}) {
Icon(Icons.Default.Save, contentDescription = "保存")
}
}
}
}, contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
Box(
Modifier
.fillMaxSize()
.padding(innerPadding)
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "更多操作")
}
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
RoundDropdownMenuItem(
text = "复制规则",
onClick = {
showMenu = false
viewModel.copyRule()
}
)
RoundDropdownMenuItem(
text = "粘贴规则",
onClick = {
showMenu = false
viewModel.pasteRule(onSuccess = {})
}
)
}
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
AppFloatingActionButton(
modifier = Modifier
.navigationBarsPadding()
.animateFloatingActionButton(
visible = !isKeyboardVisible,
alignment = Alignment.BottomEnd,
),
onClick = { viewModel.save(onSaveSuccess) },
tooltipText = "添加"
) {
AnimatedVisibility(
visible = isKeyboardVisible,
enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
),
exit = slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
),
modifier = Modifier
.align(Alignment.BottomCenter)
.zIndex(1f)
) {
QuickInputBar(
onInsert = { text -> viewModel.insertTextAtCursor(text) }
)
}
Column(
Icon(Icons.Default.Save, contentDescription = "保存")
}
}, contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
Box(
Modifier
.fillMaxSize()
.padding(innerPadding)
) {
AnimatedVisibility(
visible = isKeyboardVisible,
enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
),
exit = slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
),
modifier = Modifier
.align(Alignment.BottomCenter)
.zIndex(1f)
) {
QuickInputBar(
onInsert = { text -> viewModel.insertTextAtCursor(text) }
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
AppTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
label = "规则名称",
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Name
},
singleLine = true
)
GroupSelector(
currentGroup = state.group,
allGroups = state.allGroups,
onGroupChange = viewModel::onGroupChange,
onManageClick = { viewModel.toggleGroupDialog(true) }
)
AppTextField(
value = state.pattern,
onValueChange = viewModel::onPatternChange,
label = "匹配规则",
placeholder = { AppText("输入正则表达式或关键字") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Pattern
}
)
AppTextField(
value = state.replacement,
onValueChange = viewModel::onReplacementChange,
label = "替换为",
placeholder = { AppText("输入替换内容或捕获组") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Replacement
}
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
OutlinedTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
label = { AppText("规则名称") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Name
},
singleLine = true
)
GroupSelector(
currentGroup = state.group,
allGroups = state.allGroups,
onGroupChange = viewModel::onGroupChange,
onManageClick = { viewModel.toggleGroupDialog(true) }
)
OutlinedTextField(
value = state.pattern,
onValueChange = viewModel::onPatternChange,
label = { AppText("匹配规则") },
placeholder = { AppText("输入正则表达式或关键字") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Pattern
FilterChip(
selected = state.scopeTitle,
onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
label = { AppText("标题") },
leadingIcon = if (state.scopeTitle) {
{
Icon(
Icons.Default.Check,
contentDescription = "已选择",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
OutlinedTextField(
value = state.replacement,
onValueChange = viewModel::onReplacementChange,
label = { AppText("替换为") },
placeholder = { AppText("输入替换内容或捕获组") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Replacement
Spacer(Modifier.width(8.dp))
FilterChip(
selected = state.scopeContent,
onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
label = { AppText("内容") },
leadingIcon = if (state.scopeContent) {
{
Icon(
Icons.Default.Check,
contentDescription = "已选择",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
FilterChip(
selected = state.scopeTitle,
onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
label = { AppText("标题") },
leadingIcon = if (state.scopeTitle) {
{
Icon(
Icons.Default.Check,
contentDescription = "已选",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
Spacer(Modifier.weight(1f))
Spacer(Modifier.width(8.dp))
FilterChip(
selected = state.scopeContent,
onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
label = { AppText("内容") },
leadingIcon = if (state.scopeContent) {
{
Icon(
Icons.Default.Check,
contentDescription = "已选",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
Spacer(Modifier.weight(1f))
FilterChip(
selected = state.isRegex,
onClick = { viewModel.onRegexChange(!state.isRegex) },
label = { AppText("使用正则") },
leadingIcon = if (state.isRegex) {
{
Icon(
Icons.Default.Check,
contentDescription = "正则已启用",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
}
OutlinedTextField(
value = state.scope,
onValueChange = viewModel::onScopeChange,
label = { AppText("特定范围") },
placeholder = { AppText("指定规则适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Scope
FilterChip(
selected = state.isRegex,
onClick = { viewModel.onRegexChange(!state.isRegex) },
label = { AppText("使用正则") },
leadingIcon = if (state.isRegex) {
{
Icon(
Icons.Default.Check,
contentDescription = "正则已启用",
Modifier.size(FilterChipDefaults.IconSize)
)
}
} else null
)
OutlinedTextField(
value = state.excludeScope,
onValueChange = viewModel::onExcludeScopeChange,
label = { AppText("排除范围") },
placeholder = { AppText("指定规则不适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Exclude
}
)
OutlinedTextField(
value = state.timeout,
onValueChange = viewModel::onTimeoutChange,
label = { AppText("超时 (ms)") },
placeholder = { AppText("3000") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(120.dp))
}
AppTextField(
value = state.scope,
onValueChange = viewModel::onScopeChange,
label = "特定范围",
placeholder = { AppText("指定规则适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Scope
}
)
AppTextField(
value = state.excludeScope,
onValueChange = viewModel::onExcludeScopeChange,
label = "排除范围",
placeholder = { AppText("指定规则不适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
if (it.isFocused) viewModel.activeField =
ReplaceEditViewModel.ActiveField.Exclude
}
)
AppTextField(
value = state.timeout,
onValueChange = viewModel::onTimeoutChange,
label = "超时 (ms)",
placeholder = { AppText("3000") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(120.dp))
}
if (state.showGroupDialog) {
ManageGroupDialog(
groups = state.allGroups.filter { it != "默认" },
onDismiss = { viewModel.toggleGroupDialog(false) },
onDelete = { viewModel.deleteGroups(it) }
)
}
ManageGroupDialog(
show = state.showGroupDialog,
groups = state.allGroups.filter { it != "默认" },
onDismiss = { viewModel.toggleGroupDialog(false) },
onDelete = { viewModel.deleteGroups(it) }
)
}
}
}
@@ -380,13 +358,12 @@ fun GroupSelector(
onExpandedChange = { expanded = !expanded },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
AppTextField(
value = currentGroup,
onValueChange = onGroupChange,
label = { AppText("分组") },
label = "分组",
placeholder = { AppText("默认") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier
.fillMaxWidth()
.menuAnchor(
@@ -394,13 +371,13 @@ fun GroupSelector(
true
)
)
ExposedDropdownMenu(
RoundDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
allGroups.forEach { selectionOption ->
DropdownMenuItem(
text = { AppText(selectionOption) },
RoundDropdownMenuItem(
text = selectionOption,
onClick = {
onGroupChange(selectionOption)
expanded = false
@@ -417,52 +394,52 @@ fun GroupSelector(
@Composable
fun ManageGroupDialog(
show: Boolean, // 💡 新增 show 参数,外层直接传值,不要用 if 包裹!
groups: List<String>,
onDismiss: () -> Unit,
onDelete: (List<String>) -> Unit
) {
// 💡 使用 Set 来管理多选状态,并且以 show 作为 key。
// 这样每次重新打开弹窗时,选中状态会自动清空,防止上次的残留
var selectedGroups by remember(show) { mutableStateOf(emptySet<String>()) }
val selected = remember { mutableStateMapOf<String, Boolean>() }
AlertDialog(
AppAlertDialog(
show = show,
onDismissRequest = onDismiss,
title = { AppText("分组管理") },
text = {
if (groups.isEmpty()) AppText("暂无其他分组")
else Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
groups.forEach { group ->
val isSelected = selected[group] ?: false
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent,
shape = MaterialTheme.shapes.small
)
.clickable { selected[group] = !isSelected }
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = isSelected, onCheckedChange = null)
AppText(group, Modifier.padding(start = 8.dp))
title = "分组管理",
content = {
if (groups.isEmpty()) {
AppText("暂无其他分组")
} else {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
// 去掉了 spacedBy(8.dp),因为 CheckboxItem 内部已经有了合适的 padding
) {
groups.forEach { group ->
val isSelected = selectedGroups.contains(group)
CheckboxItem(
title = group,
checked = isSelected,
onCheckedChange = { checked ->
selectedGroups = if (checked) {
selectedGroups + group
} else {
selectedGroups - group
}
}
)
}
}
}
},
confirmButton = {
AlertButton(
onClick = {
val toDelete = selected.filter { it.value }.keys.toList()
onDelete(toDelete)
},
text = "删除选中"
)
confirmText = "删除选中",
onConfirm = {
onDelete(selectedGroups.toList())
// 注意:外层需要在 onDelete 回调中将 show 置为 false
},
dismissButton = { TextButton(onClick = onDismiss) { AppText("关闭") } }
dismissText = "关闭",
onDismiss = onDismiss
)
}
@@ -1,8 +1,6 @@
package io.legado.app.ui.replace.edit
import android.app.Application
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -26,15 +24,15 @@ import kotlinx.coroutines.launch
data class ReplaceEditUiState(
val id: Long = 0,
val name: TextFieldValue = TextFieldValue(""),
val name: String = "",
val group: String = "默认",
val pattern: TextFieldValue = TextFieldValue(""),
val replacement: TextFieldValue = TextFieldValue(""),
val pattern: String = "",
val replacement: String = "",
val isRegex: Boolean = false,
val scope: TextFieldValue = TextFieldValue(""),
val scope: String = "",
val scopeTitle: Boolean = false,
val scopeContent: Boolean = false,
val excludeScope: TextFieldValue = TextFieldValue(""),
val excludeScope: String = "",
val timeout: String = "3000",
val allGroups: List<String> = emptyList(),
val showGroupDialog: Boolean = false
@@ -71,13 +69,13 @@ class ReplaceEditViewModel(
_uiState.update {
it.copy(
id = id,
name = TextFieldValue(route.pattern ?: ""),
pattern = TextFieldValue(route.pattern ?: ""),
name = route.pattern ?: "",
pattern = route.pattern ?: "",
isRegex = route.isRegex,
scope = TextFieldValue(route.scope ?: ""),
scope = route.scope ?: "",
scopeTitle = route.isScopeTitle,
scopeContent = route.isScopeContent,
excludeScope = TextFieldValue(""),
excludeScope = "",
)
}
}
@@ -96,15 +94,15 @@ class ReplaceEditViewModel(
_uiState.update {
it.copy(
id = rule.id,
name = TextFieldValue(rule.name),
name = rule.name,
group = rule.group ?: "默认",
pattern = TextFieldValue(rule.pattern),
replacement = TextFieldValue(rule.replacement),
pattern = rule.pattern,
replacement = rule.replacement,
isRegex = rule.isRegex,
scopeTitle = rule.scopeTitle,
scopeContent = rule.scopeContent,
scope = TextFieldValue(rule.scope ?: ""),
excludeScope = TextFieldValue(rule.excludeScope ?: ""),
scope = rule.scope ?: "",
excludeScope = rule.excludeScope ?: "",
timeout = rule.timeoutMillisecond.toString()
)
}
@@ -114,15 +112,15 @@ class ReplaceEditViewModel(
val state = _uiState.value
val rule = ReplaceRule().apply {
id = state.id
name = state.name.text
name = state.name
group = if (state.group == "默认" || state.group.isBlank()) null else state.group
pattern = state.pattern.text
replacement = state.replacement.text
pattern = state.pattern
replacement = state.replacement
isRegex = state.isRegex
scopeTitle = state.scopeTitle
scopeContent = state.scopeContent
scope = state.scope.text
excludeScope = state.excludeScope.text
scope = state.scope
excludeScope = state.excludeScope
timeoutMillisecond = state.timeout.toLongOrNull() ?: 3000L
}
return rule
@@ -160,23 +158,27 @@ class ReplaceEditViewModel(
}
}
fun onNameChange(v: TextFieldValue) {
fun onNameChange(v: String) {
_uiState.update { it.copy(name = v) }
activeField = ActiveField.Name
}
fun onScopeChange(v: TextFieldValue) {
fun onScopeChange(v: String) {
_uiState.update { it.copy(scope = v) }
activeField = ActiveField.Scope
}
fun onPatternChange(v: TextFieldValue) {
fun onPatternChange(v: String) {
_uiState.update { it.copy(pattern = v) }
activeField = ActiveField.Pattern
}
fun onReplacementChange(v: TextFieldValue) {
fun onReplacementChange(v: String) {
_uiState.update { it.copy(replacement = v) }
activeField = ActiveField.Replacement
}
fun onExcludeScopeChange(v: TextFieldValue) {
fun onExcludeScopeChange(v: String) {
_uiState.update { it.copy(excludeScope = v) }
activeField = ActiveField.Exclude
}
@@ -189,56 +191,30 @@ class ReplaceEditViewModel(
fun insertTextAtCursor(text: String) {
val state = _uiState.value
when (activeField) {
ActiveField.Name -> {
val newTfv = insertIntoTextFieldValue(state.name, text)
_uiState.update { it.copy(name = newTfv) }
}
ActiveField.Pattern -> {
val newTfv = insertIntoTextFieldValue(state.pattern, text)
_uiState.update { it.copy(pattern = newTfv) }
}
ActiveField.Replacement -> {
val newTfv = insertIntoTextFieldValue(state.replacement, text)
_uiState.update { it.copy(replacement = newTfv) }
}
ActiveField.Scope -> {
val newTfv = insertIntoTextFieldValue(state.scope, text)
_uiState.update { it.copy(scope = newTfv) }
}
ActiveField.Exclude -> {
val newTfv = insertIntoTextFieldValue(state.excludeScope, text)
_uiState.update { it.copy(excludeScope = newTfv) }
}
ActiveField.Name -> _uiState.update { it.copy(name = it.name + text) }
ActiveField.Pattern -> _uiState.update { it.copy(pattern = it.pattern + text) }
ActiveField.Replacement -> _uiState.update { it.copy(replacement = it.replacement + text) }
ActiveField.Scope -> _uiState.update { it.copy(scope = it.scope + text) }
ActiveField.Exclude -> _uiState.update { it.copy(excludeScope = it.excludeScope + text) }
else -> {}
}
}
private fun insertIntoTextFieldValue(current: TextFieldValue, insert: String): TextFieldValue {
val start = current.selection.start
val end = current.selection.end
val newText = current.text.replaceRange(start, end, insert)
val newCursor = start + insert.length
return TextFieldValue(
text = newText,
selection = TextRange(newCursor)
)
}
fun save(onSuccess: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val state = _uiState.value
val rule = ReplaceRule().apply {
id = if (state.id <= 0) System.currentTimeMillis() else state.id
name = state.name.text
name = state.name
group = if (state.group == "默认" || state.group.isBlank()) null else state.group
pattern = state.pattern.text
replacement = state.replacement.text
pattern = state.pattern
replacement = state.replacement
isRegex = state.isRegex
scopeTitle = state.scopeTitle
scopeContent = state.scopeContent
scope = state.scope.text
excludeScope = state.excludeScope.text
scope = state.scope
excludeScope = state.excludeScope
timeoutMillisecond = state.timeout.toLongOrNull() ?: 3000L
}
@@ -59,47 +59,44 @@ fun RssFavoritesScreen(
var showRemoveFromGroupDialog by remember { mutableStateOf(false) }
var showSetGroupDialog by remember { mutableStateOf<RssStar?>(null) }
if (showAddToGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.add_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showAddToGroupDialog = false },
onConfirm = {
viewModel.selectionAddToGroups(state.selectedIds, it)
showAddToGroupDialog = false
viewModel.clearSelection()
}
)
}
TextListInputDialog(
show = showAddToGroupDialog,
title = stringResource(R.string.add_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showAddToGroupDialog = false },
onConfirm = { text ->
viewModel.selectionAddToGroups(state.selectedIds, text)
showAddToGroupDialog = false
viewModel.clearSelection()
}
)
if (showRemoveFromGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.remove_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showRemoveFromGroupDialog = false },
onConfirm = {
viewModel.selectionRemoveFromGroups(state.selectedIds, it)
showRemoveFromGroupDialog = false
viewModel.clearSelection()
}
)
}
TextListInputDialog(
show = showRemoveFromGroupDialog,
title = stringResource(R.string.remove_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showRemoveFromGroupDialog = false },
onConfirm = { text ->
viewModel.selectionRemoveFromGroups(state.selectedIds, text)
showRemoveFromGroupDialog = false
viewModel.clearSelection()
}
)
showSetGroupDialog?.let { rssStar ->
TextListInputDialog(
title = stringResource(R.string.change_group),
hint = stringResource(R.string.group_name),
initialValue = rssStar.group,
suggestions = groups,
onDismissRequest = { showSetGroupDialog = null },
onConfirm = {
viewModel.updateGroup(rssStar, it)
showSetGroupDialog = null
}
)
}
TextListInputDialog(
data = showSetGroupDialog,
title = stringResource(R.string.change_group),
hint = stringResource(R.string.group_name),
initialValue = { rssStar -> rssStar.group },
suggestions = groups,
onDismissRequest = { showSetGroupDialog = null },
onConfirm = { rssStar, text ->
viewModel.updateGroup(rssStar, text)
showSetGroupDialog = null
}
)
RuleListScaffold(
title = stringResource(R.string.favorite),
@@ -43,6 +43,7 @@ import io.legado.app.data.entities.RssSource
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.DraggableSelectionHandler
import io.legado.app.ui.widget.components.GroupManageBottomSheet
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.dialog.TextListInputDialog
@@ -133,44 +134,42 @@ fun RssSourceScreen(
}
)
if (showUrlInput) {
SourceInputDialog(
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
}
SourceInputDialog(
show = showUrlInput,
title = stringResource(R.string.import_on_line),
onDismissRequest = { showUrlInput = false },
onConfirm = {
showUrlInput = false
viewModel.importSource(it)
}
)
if (showAddToGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.add_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showAddToGroupDialog = false },
onConfirm = {
viewModel.selectionAddToGroups(selectedIds, it)
showAddToGroupDialog = false
viewModel.setSelection(emptySet())
}
)
}
if (showRemoveFromGroupDialog) {
TextListInputDialog(
title = stringResource(R.string.remove_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showRemoveFromGroupDialog = false },
onConfirm = {
viewModel.selectionRemoveFromGroups(selectedIds, it)
showRemoveFromGroupDialog = false
viewModel.setSelection(emptySet())
}
)
}
TextListInputDialog(
show = showAddToGroupDialog,
title = stringResource(R.string.add_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showAddToGroupDialog = false },
onConfirm = { text ->
viewModel.selectionAddToGroups(selectedIds, text)
showAddToGroupDialog = false
viewModel.setSelection(emptySet())
}
)
TextListInputDialog(
show = showRemoveFromGroupDialog,
title = stringResource(R.string.remove_group),
hint = stringResource(R.string.group_name),
suggestions = groups,
onDismissRequest = { showRemoveFromGroupDialog = false },
onConfirm = { text ->
viewModel.selectionRemoveFromGroups(selectedIds, text)
showRemoveFromGroupDialog = false
viewModel.setSelection(emptySet())
}
)
GroupManageBottomSheet(
show = showGroupManageSheet,
@@ -214,23 +213,18 @@ fun RssSourceScreen(
}
}
showDeleteRuleDialog?.let { source ->
AlertDialog(
onDismissRequest = { showDeleteRuleDialog = null },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.del_msg)) },
confirmButton = {
OutlinedButton(onClick = {
viewModel.del(source); showDeleteRuleDialog = null
}) { AppText(stringResource(R.string.ok)) }
},
dismissButton = {
TextButton(onClick = { showDeleteRuleDialog = null }) {
AppText(stringResource(R.string.cancel))
}
}
)
}
AppAlertDialog(
data = showDeleteRuleDialog,
onDismissRequest = { showDeleteRuleDialog = null },
title = stringResource(R.string.delete),
confirmText = stringResource(R.string.ok),
onConfirm = { rule ->
viewModel.del(rule)
showDeleteRuleDialog = null
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteRuleDialog = null }
)
RuleListScaffold(
title = stringResource(R.string.rss_source),
@@ -19,18 +19,13 @@ import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import io.legado.app.ui.widget.components.AppFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -53,7 +48,9 @@ import io.legado.app.ui.association.ImportBookSourceDialog
import io.legado.app.ui.association.ImportReplaceRuleDialog
import io.legado.app.ui.association.ImportRssSourceDialog
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
@@ -104,24 +101,18 @@ fun RuleSubScreen(
)
},
floatingActionButton = {
TooltipBox(
positionProvider =
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
tooltip = { PlainTooltip { AppText("Localized description") } },
state = rememberTooltipState(),
AppFloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = true,
alignment = Alignment.BottomEnd,
),
onClick = {
showEditDialog = RuleSub(customOrder = state.items.size + 1)
},
tooltipText = "Localized description"
) {
FloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = true,
alignment = Alignment.BottomEnd,
),
onClick = {
showEditDialog = RuleSub(customOrder = state.items.size + 1)
}
) {
Icon(Icons.Default.Add, contentDescription = "Add Rule")
}
Icon(Icons.Default.Add, contentDescription = "Add Rule")
}
}
) { paddingValues ->
@@ -199,80 +190,82 @@ fun RuleSubScreen(
}
}
showEditDialog?.let { ruleSub ->
RuleSubEditDialog(
ruleSub = ruleSub,
onDismiss = { showEditDialog = null },
onConfirm = { updatedRuleSub ->
viewModel.save(
updatedRuleSub,
onSuccess = { showEditDialog = null },
onError = { context.toastOnUi(it) }
)
}
)
}
RuleSubEditDialog(
ruleSub = showEditDialog,
onDismiss = { showEditDialog = null },
onConfirm = { updatedRuleSub ->
viewModel.save(
updatedRuleSub,
onSuccess = { showEditDialog = null },
onError = { context.toastOnUi(it) }
)
}
)
}
@Composable
fun RuleSubEditDialog(
ruleSub: RuleSub,
ruleSub: RuleSub?,
onDismiss: () -> Unit,
onConfirm: (RuleSub) -> Unit
) {
var name by remember { mutableStateOf(ruleSub.name) }
var url by remember { mutableStateOf(ruleSub.url) }
var type by remember { mutableIntStateOf(ruleSub.type) }
var cachedRule by remember { mutableStateOf(ruleSub) }
if (ruleSub != null) {
cachedRule = ruleSub
}
var name by remember(cachedRule) { mutableStateOf(cachedRule?.name ?: "") }
var url by remember(cachedRule) { mutableStateOf(cachedRule?.url ?: "") }
var type by remember(cachedRule) { mutableIntStateOf(cachedRule?.type ?: 0) }
val typeArray = stringArrayResource(R.array.rule_type)
AlertDialog(
AppAlertDialog(
show = ruleSub != null,
onDismissRequest = onDismiss,
title = { AppText(stringResource(R.string.rule_subscription)) },
text = {
title = stringResource(R.string.rule_subscription),
content = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
// 替换原生的 OutlinedTextField,确保双端主题适配
AppTextField(
value = name,
onValueChange = { name = it },
label = { AppText(stringResource(R.string.name)) },
label = stringResource(R.string.name),
modifier = Modifier.fillMaxWidth()
)
OutlinedTextField(
AppTextField(
value = url,
onValueChange = { url = it },
label = { AppText("URL") },
label = "URL",
modifier = Modifier.fillMaxWidth()
)
AppText(
text = "订阅类型",
style = LegadoTheme.typography.titleSmall,
modifier = Modifier.padding(top = 8.dp)
)
CheckboxGroupContainer(columns = 2) {
typeArray.forEachIndexed { index, text ->
item {
CheckboxItem(
title = text,
checked = (index == type),
onCheckedChange = {
if (it) type = index
}
onCheckedChange = { if (it) type = index }
)
}
}
}
}
},
confirmButton = {
TextButton(onClick = {
onConfirm(ruleSub.copy(name = name, url = url, type = type))
}) {
AppText(stringResource(R.string.ok))
confirmText = stringResource(R.string.ok),
onConfirm = {
cachedRule?.let {
onConfirm(it.copy(name = name, url = url, type = type))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
AppText(stringResource(R.string.cancel))
}
}
dismissText = stringResource(R.string.cancel),
onDismiss = onDismiss
)
}
@@ -1,14 +1,16 @@
package io.legado.app.ui.theme
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme.composeEngine
@Composable
fun Modifier.adaptiveHorizontalPadding(): Modifier {
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 8.dp else 16.dp
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
return this.padding(horizontal = horizontal)
}
@@ -17,3 +19,33 @@ fun Modifier.adaptiveVerticalPadding(): Modifier {
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 8.dp
return this.padding(horizontal = horizontal)
}
@Composable
fun adaptiveContentPadding(
top: Dp,
bottom: Dp
): PaddingValues {
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 8.dp else top
return PaddingValues(
top = adjustedTop,
bottom = bottom,
start = horizontal,
end = horizontal
)
}
@Composable
fun adaptiveContentPadding(
top: Dp,
bottom: Dp,
horizontal: Dp
): PaddingValues {
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top else top + 4.dp
return PaddingValues(
top = adjustedTop,
bottom = bottom,
start = horizontal,
end = horizontal
)
}
@@ -120,15 +120,15 @@ fun AppTheme(
onPrimaryContainer = miuixColorScheme.onPrimaryContainer,
inversePrimary = miuixColorScheme.primaryVariant,
secondary = miuixColorScheme.primary.copy(alpha = 0.5f),
onSecondary = miuixColorScheme.onPrimary.copy(alpha = 0.5f),
secondaryContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.5f),
onSecondaryContainer = miuixColorScheme.onPrimaryContainer.copy(alpha = 0.5f),
secondary = miuixColorScheme.secondary,
onSecondary = miuixColorScheme.onSecondary,
secondaryContainer = miuixColorScheme.secondaryContainer,
onSecondaryContainer = miuixColorScheme.onSecondaryContainer,
tertiary = miuixColorScheme.secondary,
onTertiary = miuixColorScheme.onSecondary,
tertiaryContainer = miuixColorScheme.secondaryContainer,
onTertiaryContainer = miuixColorScheme.onSecondaryContainer,
tertiary = miuixColorScheme.primary,
onTertiary = miuixColorScheme.onPrimary,
tertiaryContainer = miuixColorScheme.primaryContainer,
onTertiaryContainer = miuixColorScheme.primaryVariant,
// ================= 4. 背景与表面 (Background & Surface) =================
background = miuixColorScheme.background,
@@ -58,8 +58,6 @@ object ThemeResolver {
fun isMiuixEngine(composeEngine: String): Boolean = composeEngine == "miuix"
fun isM3Engine(composeEngine: String): Boolean = composeEngine == "m3"
fun resolveColorSpecVersion(colorSpec: ThemeColorSpec): ColorSpec.SpecVersion {
return when (colorSpec) {
ThemeColorSpec.SPEC_2025 -> ColorSpec.SpecVersion.SPEC_2025
@@ -4,6 +4,7 @@ import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.KeyboardActionHandler
@@ -12,16 +13,14 @@ import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TextFieldLabelPosition
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Density
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
@@ -36,8 +35,7 @@ fun AppTextField(
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle? = null,
textColor: Color = Color.Unspecified,
backgroundColor: Color = Color.Unspecified,
label: String? = null,
labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
placeholder: @Composable (() -> Unit)? = null,
@@ -55,16 +53,9 @@ fun AppTextField(
onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null,
scrollState: ScrollState = rememberScrollState(),
shape: Shape = TextFieldDefaults.shape,
colors: TextFieldColors = TextFieldDefaults.colors(),
contentPadding: PaddingValues? = null,
interactionSource: MutableInteractionSource? = null,
) {
val baseStyle = textStyle ?: LegadoTheme.typography.bodyMedium
val defaultTextColor = LegadoTheme.colorScheme.onSurface
val finalTextColor = textColor.takeOrElse {
baseStyle.color.takeOrElse { defaultTextColor }
}
val mergedTextStyle = baseStyle.merge(color = finalTextColor)
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
if (isMiuix) {
@@ -73,8 +64,7 @@ fun AppTextField(
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = MiuixTheme.textStyles.main,
backgroundColor = MiuixTheme.colorScheme.surfaceContainerHigh,
backgroundColor = if (backgroundColor != Color.Unspecified) backgroundColor else MiuixTheme.colorScheme.surfaceContainerHigh,
label = label ?: "",
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
@@ -94,12 +84,26 @@ fun AppTextField(
} else {
TextFieldDefaults.contentPaddingWithLabel()
}
val resolvedColors = if (backgroundColor != Color.Unspecified) {
TextFieldDefaults.colors(
focusedContainerColor = backgroundColor,
unfocusedContainerColor = backgroundColor,
disabledContainerColor = backgroundColor,
errorContainerColor = backgroundColor,
focusedIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().focusedIndicatorColor,
unfocusedIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().unfocusedIndicatorColor,
disabledIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().disabledIndicatorColor,
)
} else {
TextFieldDefaults.colors()
}
TextField(
state = state,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
labelPosition = labelPosition,
label = label?.let { { AppText(it) } },
placeholder = placeholder,
@@ -117,9 +121,204 @@ fun AppTextField(
onTextLayout = onTextLayout,
scrollState = scrollState,
shape = shape,
colors = colors,
colors = resolvedColors,
contentPadding = resolvedContentPadding,
interactionSource = interactionSource
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
backgroundColor: Color = Color.Unspecified,
label: String? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
shape: Shape = TextFieldDefaults.shape,
interactionSource: MutableInteractionSource? = null,
) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
if (isMiuix) {
MiuixTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
backgroundColor = if (backgroundColor != Color.Unspecified) backgroundColor else MiuixTheme.colorScheme.surfaceContainerHigh,
label = label ?: "",
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
interactionSource = interactionSource
)
} else {
val resolvedColors = if (backgroundColor != Color.Unspecified) {
TextFieldDefaults.colors(
focusedContainerColor = backgroundColor,
unfocusedContainerColor = backgroundColor,
disabledContainerColor = backgroundColor,
errorContainerColor = backgroundColor,
focusedIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().focusedIndicatorColor,
unfocusedIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().unfocusedIndicatorColor,
disabledIndicatorColor = if (backgroundColor == Color.Transparent) Color.Transparent else TextFieldDefaults.colors().disabledIndicatorColor,
)
} else {
TextFieldDefaults.colors()
}
TextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
label = label?.let { { AppText(it) } },
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
shape = shape,
colors = resolvedColors,
interactionSource = interactionSource
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTextFieldSurface(
state: TextFieldState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
label: String? = null,
labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
inputTransformation: InputTransformation? = null,
outputTransformation: OutputTransformation? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
onKeyboardAction: KeyboardActionHandler? = null,
lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null,
scrollState: ScrollState = rememberScrollState(),
shape: Shape = TextFieldDefaults.shape,
contentPadding: PaddingValues? = null,
interactionSource: MutableInteractionSource? = null,
) {
AppTextField(
state = state,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
backgroundColor = LegadoTheme.colorScheme.surface,
label = label,
labelPosition = labelPosition,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
isError = isError,
inputTransformation = inputTransformation,
outputTransformation = outputTransformation,
keyboardOptions = keyboardOptions,
onKeyboardAction = onKeyboardAction,
lineLimits = lineLimits,
onTextLayout = onTextLayout,
scrollState = scrollState,
shape = shape,
contentPadding = contentPadding,
interactionSource = interactionSource
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTextFieldSurface(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
label: String? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
prefix: @Composable (() -> Unit)? = null,
suffix: @Composable (() -> Unit)? = null,
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
shape: Shape = TextFieldDefaults.shape,
interactionSource: MutableInteractionSource? = null,
) {
AppTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
readOnly = readOnly,
backgroundColor = LegadoTheme.colorScheme.surface,
label = label,
placeholder = placeholder,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
isError = isError,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
shape = shape,
interactionSource = interactionSource
)
}
@@ -16,7 +16,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -30,7 +29,6 @@ import io.legado.app.R
import io.legado.app.ui.widget.components.button.SmallTextButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.SettingItem
import io.legado.app.ui.widget.components.text.AppText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -98,12 +96,12 @@ private fun GroupItem(
}
},
expandContent = {
TextField(
AppTextField(
state = state,
modifier = Modifier
.fillMaxWidth()
.heightIn(min = 48.dp),
label = { AppText(stringResource(R.string.edit)) },
label = stringResource(R.string.edit),
contentPadding = PaddingValues(
top = 4.dp,
bottom = 4.dp,
@@ -27,10 +27,12 @@ import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.CoroutineScope
import top.yukonga.miuix.kmp.theme.MiuixTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBarSection(
modifier: Modifier = Modifier,
query: String,
onQueryChange: (String) -> Unit,
placeholder: String = "搜索...",
@@ -69,21 +71,25 @@ fun SearchBarSection(
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
val modifier = modifier
.fillMaxWidth()
.then(
if (isMiuix) modifier.padding(vertical = 4.dp)
else modifier
)
val searchTextField = @Composable {
AppTextField(
state = textFieldState,
modifier = Modifier
.fillMaxWidth(),
modifier = modifier,
placeholder = { AppText(placeholder) },
leadingIcon = leadingIcon,
lineLimits = TextFieldLineLimits.SingleLine,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
)
backgroundColor = if (!isMiuix) {
Color.Transparent
} else {
MiuixTheme.colorScheme.surfaceContainer
}
)
}
@@ -93,7 +99,7 @@ fun SearchBarSection(
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
.padding(bottom = 4.dp),
shape = RoundedCornerShape(32.dp),
color = backgroundColor
) {
@@ -0,0 +1,170 @@
package io.legado.app.ui.widget.components.alert
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.button.MiuixPrimaryButton
import io.legado.app.ui.widget.components.button.MiuixSecondaryButton
import top.yukonga.miuix.kmp.window.WindowDialog
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppAlertDialog(
modifier: Modifier = Modifier,
show: Boolean,
onDismissRequest: () -> Unit,
title: String? = null,
text: String? = null,
content: (@Composable () -> Unit)? = null,
confirmText: String = "确定", // 默认文字
onConfirm: (() -> Unit)? = null,
dismissText: String = "取消",
onDismiss: (() -> Unit)? = null,
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
WindowDialog(
show = show,
modifier = modifier,
title = title,
summary = text,
onDismissRequest = onDismissRequest,
content = {
if (content != null) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
) {
content()
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (onDismiss != null) {
MiuixSecondaryButton(
text = dismissText,
modifier = Modifier.weight(1f),
onClick = {
onDismiss()
onDismissRequest()
}
)
}
if (onConfirm != null) {
MiuixPrimaryButton(
text = confirmText,
modifier = Modifier.weight(1f),
onClick = {
onConfirm()
}
)
}
}
}
)
} else {
if (show) {
AlertDialog(
onDismissRequest = onDismissRequest,
modifier = modifier,
title = title?.let { { Text(text = it) } },
text = {
Column {
if (text != null) {
Text(
text = text,
modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp)
)
}
if (content != null) {
content()
}
}
},
confirmButton = {
if (onConfirm != null) {
OutlinedButton(
onClick = onConfirm
) {
Text(text = confirmText)
}
}
},
dismissButton = {
if (onDismiss != null) {
TextButton(
onClick = {
onDismiss()
onDismissRequest()
}
) {
Text(text = dismissText)
}
}
}
)
}
}
}
/**
* 专为 nullable 数据设计的 AppAlertDialog 重载。
* 当 [data] 不为 null 时显示弹窗;当 [data] 变为 null 时,自动缓存最后一次数据并播放退出动画。
*/
@Composable
fun <T> AppAlertDialog(
data: T?,
onDismissRequest: () -> Unit,
title: String? = null,
confirmText: String = "确定",
onConfirm: ((T) -> Unit)? = null,
dismissText: String = "取消",
onDismiss: (() -> Unit)? = null,
modifier: Modifier = Modifier,
content: (@Composable (T) -> Unit)? = null
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) {
cachedData = data
}
val currentData = cachedData
if (currentData != null) {
AppAlertDialog(
show = data != null,
onDismissRequest = onDismissRequest,
title = title,
modifier = modifier,
confirmText = confirmText,
onConfirm = onConfirm?.let { { it(currentData) } },
dismissText = dismissText,
onDismiss = onDismiss,
content = content?.let { { it(currentData) } }
)
}
}
@@ -7,12 +7,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -22,10 +17,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.Bookmark
import io.legado.app.ui.widget.components.AppTextFieldSurface
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.PrimaryButton
import io.legado.app.ui.widget.components.button.SecondaryButton
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -38,8 +34,8 @@ fun BookmarkEditSheet(
) {
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
var bookText by remember { mutableStateOf(bookmark.bookText) }
var content by remember { mutableStateOf(bookmark.content) }
var bookText by remember(bookmark) { mutableStateOf(bookmark.bookText) }
var content by remember(bookmark) { mutableStateOf(bookmark.content) }
AppModalBottomSheet(
show = show,
@@ -51,20 +47,20 @@ fun BookmarkEditSheet(
.fillMaxWidth()
.navigationBarsPadding()
) {
OutlinedTextField(
AppTextFieldSurface(
value = bookText,
onValueChange = { bookText = it },
label = { AppText("原文") },
label = "原文",
modifier = Modifier.fillMaxWidth(),
maxLines = 10
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
AppTextFieldSurface(
value = content,
onValueChange = { content = it },
label = { AppText("摘要/笔记") },
label = "摘要/笔记",
modifier = Modifier.fillMaxWidth(),
maxLines = 5
)
@@ -98,29 +94,17 @@ fun BookmarkEditSheet(
}
}
if (showDeleteConfirmDialog) {
AlertDialog(
onDismissRequest = { showDeleteConfirmDialog = false },
title = { AppText("确认删除") },
text = { AppText("你确定要删除这条书签吗?") },
confirmButton = {
TextButton(
onClick = {
showDeleteConfirmDialog = false
onDelete(bookmark)
},
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)
) {
AppText("删除")
}
},
dismissButton = {
TextButton(
onClick = { showDeleteConfirmDialog = false }
) {
AppText("取消")
}
}
)
}
AppAlertDialog(
show = showDeleteConfirmDialog,
onDismissRequest = { showDeleteConfirmDialog = false },
title = "确认删除",
text = "你确定要删除这条书签吗?",
confirmText = "删除",
onConfirm = {
showDeleteConfirmDialog = false
onDelete(bookmark)
},
dismissText = "取消",
onDismiss = { showDeleteConfirmDialog = false }
)
}
@@ -52,7 +52,7 @@ fun BookmarkItem(
AppText(
text = bookmark.chapterName,
style = LegadoTheme.typography.titleSmallEmphasized,
color = if (isDur) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.secondary
color = if (isDur) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurface
)
if (bookmark.bookText.isNotEmpty()) {
@@ -26,4 +26,5 @@ fun AlertButton(
) {
AppText(text = text)
}
}
}
@@ -1,86 +0,0 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AnimatedActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
iconChecked: ImageVector,
iconUnchecked: ImageVector,
activeText: String,
inactiveText: String
) {
var showText by remember { mutableStateOf(false) }
var lastCheckedState by remember { mutableStateOf(checked) }
LaunchedEffect(showText) {
if (showText) {
delay(1000)
showText = false
}
}
ToggleButton(
contentPadding = PaddingValues(4.dp),
checked = checked,
onCheckedChange = {
lastCheckedState = it
onCheckedChange(it)
showText = true
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.padding(horizontal = 4.dp)
) {
AnimatedContent(
targetState = checked,
label = "IconAnimation"
) { targetChecked ->
Icon(
imageVector = if (targetChecked) iconChecked else iconUnchecked,
contentDescription = null
)
}
AnimatedVisibility(
visible = showText
) {
AppText(
text = if (lastCheckedState) activeText else inactiveText,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
}
}
@@ -0,0 +1,332 @@
package io.legado.app.ui.widget.components.button
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedIconToggleButton
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import kotlinx.coroutines.delay
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
fun MediumIconButton(
onClick: () -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixIconButton(
onClick = onClick,
modifier = modifier
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
IconButton(
onClick = onClick,
modifier = modifier
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
}
}
@Composable
fun MediumOutlinedIconButton(
onClick: () -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
OutlinedIconButton(
onClick = onClick,
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
}
}
@Composable
fun MediumOutlinedButton(
onClick: () -> Unit,
icon: ImageVector,
text: String,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
MiuixText(text = text)
}
}
} else {
OutlinedIconButton(
onClick = onClick,
border = ButtonDefaults.outlinedButtonBorder()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
Text(text = text)
}
}
}
}
@Composable
fun MediumTonalIconButton(
onClick: () -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
MiuixIconButton(
onClick = onClick,
modifier = modifier,
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription
)
}
} else {
FilledTonalIconButton(
onClick = onClick,
modifier = modifier.size(40.dp),
colors = IconButtonDefaults.filledTonalIconButtonColors()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
}
}
@Composable
fun MediumOutlinedIconToggleButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
icon: ImageVector,
modifier: Modifier = Modifier,
contentDescription: String? = null
) {
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
val containerColor by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixToggleContainerColor"
)
val iconTint by animateColorAsState(
targetValue = if (checked) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant,
animationSpec = tween(150),
label = "MiuixToggleIconTint"
)
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier,
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconTint
)
}
} else {
OutlinedIconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier
) {
Icon(
imageVector = icon,
contentDescription = contentDescription
)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun MediumAnimatedActionButton(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
iconChecked: ImageVector,
iconUnchecked: ImageVector,
activeText: String,
inactiveText: String,
modifier: Modifier = Modifier
) {
var showText by remember { mutableStateOf(false) }
var lastCheckedState by remember { mutableStateOf(checked) }
LaunchedEffect(showText) {
if (showText) {
delay(1000)
showText = false
}
}
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
MiuixIconButton(
onClick = {
lastCheckedState = !checked
onCheckedChange(!checked)
showText = true
},
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp)
) {
MiuixIcon(
tint = contentColor,
imageVector = if (checked) iconChecked else iconUnchecked,
contentDescription = null
)
AnimatedVisibility(
visible = showText
) {
MiuixText(
text = if (lastCheckedState) activeText else inactiveText,
color = contentColor,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
}
} else {
TonalToggleButton(
checked = checked,
onCheckedChange = {
lastCheckedState = it
onCheckedChange(it)
showText = true
},
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
AnimatedIcon(
imageVector = if (checked) iconChecked else iconUnchecked,
contentDescription = null
)
AnimatedVisibility(
visible = showText
) {
Text(
text = if (lastCheckedState) activeText else inactiveText,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
}
}
}
@@ -11,7 +11,10 @@ import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.extended.Back
import top.yukonga.miuix.kmp.theme.LocalContentColor
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Button as MiuixButton
import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@Composable
fun MiuixIconButton(
@@ -55,3 +58,41 @@ fun MiuixToggleButton(
)
}
}
@Composable
fun MiuixPrimaryButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
text: String
) {
MiuixButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = MiuixButtonDefaults.buttonColorsPrimary()
) {
MiuixText(
text = text
)
}
}
@Composable
fun MiuixSecondaryButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
text: String
) {
MiuixButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = MiuixButtonDefaults.buttonColors()
) {
MiuixText(
text = text
)
}
}
@@ -19,7 +19,8 @@ import androidx.compose.material3.IconToggleButtonShapes
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedIconToggleButton
import androidx.compose.material3.OutlinedToggleButton
import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@@ -34,11 +35,30 @@ import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.delay
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText
import top.yukonga.miuix.kmp.theme.MiuixTheme
private val SmallMiuixButtonSize = 32.dp
private val SmallMiuixIconSize = 18.dp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun smallContainerSize() = IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private val smallIconSize: androidx.compose.ui.unit.Dp
get() = IconButtonDefaults.extraSmallIconSize
@Composable
private fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
content()
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -57,20 +77,16 @@ fun SmallIconButton(
)
}
} else {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
SmallNoMinTouchTarget {
IconButton(
onClick = onClick,
modifier = Modifier.size(
IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
),
modifier = Modifier.size(smallContainerSize()),
shape = IconButtonDefaults.extraSmallRoundShape,
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(IconButtonDefaults.extraSmallIconSize),
modifier = Modifier.size(smallIconSize),
)
}
}
@@ -90,37 +106,31 @@ fun SmallOutlinedIconButton(
if (isMiuix) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(32.dp),
modifier = modifier.size(SmallMiuixButtonSize),
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
OutlinedIconButton(
onClick = onClick,
modifier = Modifier.size(
IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
SmallNoMinTouchTarget {
OutlinedIconButton(
onClick = onClick,
modifier = Modifier.size(smallContainerSize()),
shapes = IconButtonDefaults.shapes(),
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize)
)
),
shapes = IconButtonDefaults.shapes(),
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(
IconButtonDefaults.extraSmallIconSize
)
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -136,31 +146,27 @@ fun SmallTonalIconButton(
if (isMiuix) {
MiuixIconButton(
onClick = onClick,
modifier = modifier.size(32.dp),
modifier = modifier.size(SmallMiuixButtonSize),
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(SmallMiuixIconSize)
)
}
} else {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
SmallNoMinTouchTarget {
FilledTonalIconButton(
onClick = onClick,
modifier = modifier.size(
IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
),
modifier = modifier.size(smallContainerSize()),
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.filledTonalIconButtonColors()
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(IconButtonDefaults.extraSmallIconSize)
modifier = Modifier.size(smallIconSize)
)
}
}
@@ -195,14 +201,14 @@ fun SmallOutlinedIconToggleButton(
MiuixIconButton(
onClick = { onCheckedChange(!checked) },
modifier = modifier
.size(32.dp),
.size(SmallMiuixButtonSize),
backgroundColor = containerColor
) {
MiuixIcon(
imageVector = icon,
contentDescription = contentDescription,
tint = iconTint,
modifier = Modifier.size(18.dp)
modifier = Modifier.size(SmallMiuixIconSize)
)
}
@@ -223,17 +229,13 @@ fun SmallOutlinedIconToggleButton(
OutlinedIconToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier.size(
IconButtonDefaults.extraSmallContainerSize(
IconButtonDefaults.IconButtonWidthOption.Uniform
)
),
modifier = modifier.size(smallContainerSize()),
shapes = toggleShapes
) {
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(IconButtonDefaults.extraSmallIconSize),
modifier = Modifier.size(smallIconSize),
)
}
}
@@ -261,23 +263,29 @@ fun SmallAnimatedActionButton(
}
}
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
OutlinedToggleButton(
checked = checked,
onCheckedChange = {
lastCheckedState = it
onCheckedChange(it)
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
if (isMiuix) {
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainer,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
MiuixIconButton(
onClick = {
lastCheckedState = !checked
onCheckedChange(!checked)
showText = true
},
modifier = modifier.height(36.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
) {
AnimatedIcon(
modifier = Modifier.size(16.dp),
MiuixIcon(
imageVector = if (checked) iconChecked else iconUnchecked,
contentDescription = null
)
@@ -285,7 +293,7 @@ fun SmallAnimatedActionButton(
AnimatedVisibility(
visible = showText
) {
AppText(
MiuixText(
text = if (lastCheckedState) activeText else inactiveText,
style = LegadoTheme.typography.labelSmall,
modifier = Modifier.padding(start = 6.dp),
@@ -295,5 +303,41 @@ fun SmallAnimatedActionButton(
}
}
}
} else {
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
TonalToggleButton(
checked = checked,
onCheckedChange = {
lastCheckedState = it
onCheckedChange(it)
showText = true
},
modifier = modifier.height(36.dp),
contentPadding = PaddingValues(horizontal = 8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
AnimatedIcon(
modifier = Modifier.size(16.dp),
imageVector = if (checked) iconChecked else iconUnchecked,
contentDescription = null
)
AnimatedVisibility(
visible = showText
) {
Text(
text = if (lastCheckedState) activeText else inactiveText,
style = LegadoTheme.typography.labelSmall,
modifier = Modifier.padding(start = 6.dp),
maxLines = 1,
softWrap = false
)
}
}
}
}
}
}
}
@@ -2,6 +2,8 @@ package io.legado.app.ui.widget.components.button
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
@@ -22,6 +24,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -32,14 +35,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import kotlinx.coroutines.delay
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Text as MiuixText
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
enum class TopBarButtonVariant {
Filled, Outlined, Icon
@@ -105,19 +113,18 @@ fun TopBarButton(
}
@Composable
fun TopbarNavigationButton(
fun TopBarNavigationButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
imageVector: ImageVector = AppIcons.Back,
contentDescription: String? = "返回",
contentDescription: String? = stringResource(id = R.string.back),
style: TopBarButtonVariant = TopBarButtonVariant.Filled
) {
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
MiuixIconButton(
onClick = onClick,
imageVector = imageVector,
contentDescription = contentDescription,
modifier = modifier.padding(horizontal = 16.dp)
contentDescription = contentDescription
)
} else {
TopBarButton(
@@ -130,6 +137,27 @@ fun TopbarNavigationButton(
}
}
@Deprecated(
message = "Use TopBarNavigationButton for consistent TopBar naming.",
replaceWith = ReplaceWith("TopBarNavigationButton(onClick, modifier, imageVector, contentDescription, style)")
)
@Composable
fun TopbarNavigationButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
imageVector: ImageVector = AppIcons.Back,
contentDescription: String? = stringResource(id = R.string.back),
style: TopBarButtonVariant = TopBarButtonVariant.Filled
) {
TopBarNavigationButton(
onClick = onClick,
modifier = modifier,
imageVector = imageVector,
contentDescription = contentDescription,
style = style
)
}
@Composable
fun TopBarActionButton(
onClick: () -> Unit,
@@ -143,7 +171,7 @@ fun TopBarActionButton(
onClick = onClick,
imageVector = imageVector,
contentDescription = contentDescription,
modifier = modifier.padding(end = 8.dp),
modifier = modifier,
)
} else {
if (enableProgressive) {
@@ -186,12 +214,52 @@ fun TopBarAnimatedActionButton(
}
}
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
MiuixToggleButton(
checked = checked,
onCheckedChange = onCheckedChange,
iconChecked = iconChecked,
iconUnchecked = iconUnchecked
val containerColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
val contentColor by animateColorAsState(
targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
animationSpec = tween(150),
label = "MiuixActionButtonContainer"
)
MiuixIconButton(
onClick = {
lastCheckedState = !checked
onCheckedChange(!checked)
showText = true
},
backgroundColor = containerColor
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 8.dp)
) {
MiuixIcon(
tint = contentColor,
imageVector = if (checked) iconChecked else iconUnchecked,
contentDescription = null
)
AnimatedVisibility(
visible = showText
) {
MiuixText(
text = if (lastCheckedState) activeText else inactiveText,
color = contentColor,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
maxLines = 1,
softWrap = false
)
}
}
}
} else {
ToggleButton(
modifier = modifier.height(36.dp),
@@ -222,7 +290,7 @@ fun TopBarAnimatedActionButton(
AnimatedVisibility(
visible = showText
) {
AppText(
Text(
text = if (lastCheckedState) activeText else inactiveText,
style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(start = 8.dp),
@@ -255,4 +323,4 @@ fun AnimatedIcon(
modifier = modifier
)
}
}
}
@@ -16,6 +16,7 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.utils.PressFeedbackType
import top.yukonga.miuix.kmp.basic.Card as MiuixCard
import top.yukonga.miuix.kmp.basic.CardDefaults as MiuixCardDefaults
@@ -26,6 +27,7 @@ private fun BaseCard(
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
@@ -41,6 +43,8 @@ private fun BaseCard(
MiuixCard(
modifier = modifier,
cornerRadius = cornerRadius,
pressFeedbackType = pressFeedbackType,
showIndication = true,
onClick = onClick,
content = content,
colors = colors
@@ -84,6 +88,7 @@ fun GlassCard(
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
@@ -95,6 +100,7 @@ fun GlassCard(
onClick = onClick,
shape = shape,
cornerRadius = cornerRadius,
pressFeedbackType = pressFeedbackType,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
@@ -111,6 +117,7 @@ fun NormalCard(
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
@@ -122,6 +129,7 @@ fun NormalCard(
onClick = onClick,
shape = shape,
cornerRadius = cornerRadius,
pressFeedbackType = pressFeedbackType,
containerColor = containerColor,
contentColor = contentColor,
elevation = elevation,
@@ -0,0 +1,60 @@
package io.legado.app.ui.widget.components.checkBox
import androidx.compose.material3.Checkbox
import androidx.compose.material3.TriStateCheckbox
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.state.ToggleableState
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.Checkbox as MiuixCheckbox
@Composable
fun AppCheckbox(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
val state = if (checked) ToggleableState.On else ToggleableState.Off
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixCheckbox(
state = state,
onClick = onCheckedChange?.let { { it(!checked) } },
modifier = modifier,
enabled = enabled
)
} else {
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled
)
}
}
@Composable
fun AppTriStateCheckbox(
state: ToggleableState,
onClick: (() -> Unit)?,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
MiuixCheckbox(
state = state,
onClick = onClick,
modifier = modifier,
enabled = enabled
)
} else {
TriStateCheckbox(
state = state,
onClick = onClick,
modifier = modifier,
enabled = enabled
)
}
}
@@ -5,9 +5,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import io.legado.app.ui.widget.components.text.AppText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -16,6 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.text.AppText
@Composable
fun CheckboxItem(
@@ -32,7 +31,7 @@ fun CheckboxItem(
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (checked && enabled) MaterialTheme.colorScheme.secondaryContainer
color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer
else color,
shape = MaterialTheme.shapes.small
)
@@ -40,7 +39,7 @@ fun CheckboxItem(
.padding(vertical = 12.dp, horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
AppCheckbox(
checked = checked,
onCheckedChange = null,
enabled = enabled,
@@ -48,7 +47,7 @@ fun CheckboxItem(
)
AppText(
text = title,
style = LegadoTheme.typography.bodyMedium,
style = LegadoTheme.typography.bodyMediumEmphasized,
maxLines = 1,
modifier = Modifier
.padding(start = 12.dp)
@@ -41,6 +41,7 @@ import androidx.core.graphics.withSave
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.theme.LegadoTheme
import org.koin.compose.koinInject
import io.legado.app.model.BookCover as BookCoverModel
@@ -91,7 +92,7 @@ fun BookCover(
.clip(RoundedCornerShape(4.dp))
.then(
if (!hasCustomDefault && !isOnlineCoverLoaded) {
Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
Modifier.background(LegadoTheme.colorScheme.surfaceContainer)
} else Modifier
)
) {
@@ -145,7 +146,7 @@ fun BookCover(
Icon(
Icons.Default.Book,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
tint = LegadoTheme.colorScheme.secondary,
modifier = Modifier
.size(iconSizeDp)
.align(Alignment.Center)
@@ -14,7 +14,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Book
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -26,6 +25,7 @@ import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.ui.theme.LegadoTheme
import org.koin.compose.koinInject
@Composable
@@ -43,14 +43,14 @@ fun Cover(
val boxModifier = modifier
.aspectRatio(3f / 4f)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceContainer)
.background(LegadoTheme.colorScheme.surfaceContainer)
Box(modifier = boxModifier) {
if (path == null) {
Icon(
Icons.Default.Book,
contentDescription = null,
tint = MaterialTheme.colorScheme.surfaceContainerHighest,
tint = LegadoTheme.colorScheme.surfaceContainerHighest,
modifier = Modifier
.size(24.dp)
.align(Alignment.Center)
@@ -80,8 +80,8 @@ fun Cover(
.align(Alignment.TopEnd)
.padding(2.dp),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
color = LegadoTheme.colorScheme.secondaryContainer,
contentColor = LegadoTheme.colorScheme.onSecondaryContainer,
tonalElevation = 2.dp
) {
Row(
@@ -21,10 +21,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.text.AppText
/**
* 基础版:由 Boolean 控制显示/隐藏
*/
@Composable
fun TextListInputDialog(
show: Boolean,
title: String,
hint: String,
initialValue: String = "",
@@ -32,17 +38,18 @@ fun TextListInputDialog(
onDismissRequest: () -> Unit,
onConfirm: (String) -> Unit
) {
var text by remember { mutableStateOf(initialValue) }
var text by remember(show) { mutableStateOf(initialValue) }
AlertDialog(
AppAlertDialog(
show = show,
onDismissRequest = onDismissRequest,
title = { AppText(title) },
text = {
title = title,
content = {
Column {
OutlinedTextField(
AppTextField(
value = text,
onValueChange = { text = it },
label = { AppText(hint) },
label = hint,
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
@@ -68,17 +75,36 @@ fun TextListInputDialog(
}
}
},
confirmButton = {
TextButton(
onClick = { onConfirm(text) }
) {
AppText(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
AppText(stringResource(android.R.string.cancel))
}
}
confirmText = stringResource(android.R.string.ok),
onConfirm = { onConfirm(text) },
dismissText = stringResource(android.R.string.cancel),
onDismiss = onDismissRequest
)
}
@Composable
fun <T> TextListInputDialog(
data: T?,
title: String,
hint: String,
initialValue: (T) -> String,
suggestions: List<String> = emptyList(),
onDismissRequest: () -> Unit,
onConfirm: (T, String) -> Unit
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) cachedData = data
val currentData = cachedData
if (currentData != null) {
TextListInputDialog(
show = data != null,
title = title,
hint = hint,
initialValue = initialValue(currentData),
suggestions = suggestions,
onDismissRequest = onDismissRequest,
onConfirm = { text -> onConfirm(currentData, text) }
)
}
}
@@ -29,8 +29,9 @@ fun PillDivider(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp)
.height(4.dp)
.background(MiuixTheme.colorScheme.secondaryContainer)
.background(MiuixTheme.colorScheme.dividerLine)
)
} else {
Box(
@@ -1,34 +1,17 @@
package io.legado.app.ui.widget.components.filePicker
import android.webkit.MimeTypeMap
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.filled.CloudUpload
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
enum class FilePickerSheetMode {
DIR, FILE, EXPORT
@@ -47,100 +30,48 @@ fun FilePickerSheet(
onUpload: (() -> Unit)? = null,
allowExtensions: Array<String>? = null,
) {
AppModalBottomSheet(
OptionSheet(
show = show,
title = title,
onDismissRequest = onDismissRequest,
containerColor = LegadoTheme.colorScheme.surface
title = title
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
maxItemsInEachRow = 2
) {
onSelectSysDir?.let {
FilePickerOptionCard(
icon = Icons.Default.FolderOpen,
text = stringResource(R.string.sys_folder_picker),
onClick = it
)
}
onSelectSysFile?.let {
FilePickerOptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = stringResource(R.string.sys_file_picker),
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
onSelectSysFiles?.let {
FilePickerOptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = "多选项目",
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
onManualInput?.let {
FilePickerOptionCard(
icon = Icons.Default.EditNote,
text = stringResource(R.string.manual_input),
onClick = it
)
}
onUpload?.let {
FilePickerOptionCard(
icon = Icons.Default.CloudUpload,
text = stringResource(R.string.upload_url),
onClick = it
)
}
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
@Composable
private fun RowScope.FilePickerOptionCard(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
Surface(
onClick = onClick,
modifier = Modifier
.weight(1f)
.height(100.dp),
shape = MaterialTheme.shapes.medium,
color = LegadoTheme.colorScheme.surfaceContainerLow,
tonalElevation = 2.dp
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AppIcon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = LegadoTheme.colorScheme.primary
onSelectSysDir?.let {
OptionCard(
icon = Icons.Default.FolderOpen,
text = stringResource(R.string.sys_folder_picker),
onClick = it
)
Spacer(modifier = Modifier.height(8.dp))
AppText(
text = text,
style = LegadoTheme.typography.labelMedium,
maxLines = 1
}
onSelectSysFile?.let {
OptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = stringResource(R.string.sys_file_picker),
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
onSelectSysFiles?.let {
OptionCard(
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
text = "多选项目",
onClick = { it(typesOfExtensions(allowExtensions)) }
)
}
onManualInput?.let {
OptionCard(
icon = Icons.Default.EditNote,
text = stringResource(R.string.manual_input),
onClick = it
)
}
onUpload?.let {
OptionCard(
icon = Icons.Default.CloudUpload,
text = stringResource(R.string.upload_url),
onClick = it
)
}
}
@@ -19,12 +19,8 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccessTime
import androidx.compose.material.icons.filled.FormatListNumbered
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -41,7 +37,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.AnimatedActionButton
import io.legado.app.ui.widget.components.button.MediumAnimatedActionButton
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
import io.legado.app.ui.widget.components.text.AppText
import java.time.LocalDate
@@ -64,7 +61,7 @@ fun HeatmapCalendarTopBar(
AppText("时间线", style = LegadoTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
AnimatedActionButton(
MediumAnimatedActionButton(
checked = currentMode == HeatmapMode.TIME,
onCheckedChange = {
onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT)
@@ -77,16 +74,10 @@ fun HeatmapCalendarTopBar(
Spacer(modifier = Modifier.width(8.dp))
OutlinedIconButton(
MediumOutlinedIconButton(
onClick = onClearDate,
shapes = IconButtonDefaults.shapes(),
border = ButtonDefaults.outlinedButtonBorder()
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = "清除选择"
)
}
icon = Icons.Outlined.Delete,
)
}
}
}
@@ -111,7 +102,7 @@ fun WeekdayLabelsColumn(
AppText(
text = label,
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = LegadoTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.height(cellSize)
)
} else {
@@ -256,7 +247,7 @@ fun HeatmapWeekColumn(
AppText(
text = "${firstDayOfMonth.monthValue}",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = LegadoTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.align(Alignment.TopStart)
.wrapContentWidth(unbounded = true)
@@ -6,6 +6,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import io.legado.app.ui.theme.LegadoTheme
import java.time.LocalDate
/**
@@ -104,8 +105,8 @@ fun rememberHeatmapLevel(
@Composable
fun heatmapColorForLevel(
level: Int,
primary: Color = MaterialTheme.colorScheme.primary,
emptyColor: Color = MaterialTheme.colorScheme.surfaceVariant
primary: Color = LegadoTheme.colorScheme.primary,
emptyColor: Color = LegadoTheme.colorScheme.surface
): Color {
return when (level) {
0 -> emptyColor
@@ -19,7 +19,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Button
import androidx.compose.material3.CenterAlignedTopAppBar
@@ -28,11 +27,9 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedToggleButton
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -49,6 +46,8 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -58,24 +57,26 @@ import kotlinx.coroutines.launch
@Composable
fun SourceInputDialog(
show: Boolean,
title: String = "网络导入",
hint: String = "请输入 URL 或 JSON",
initialValue: String = "",
historyValues: List<String> = emptyList(), // 历史记录
historyValues: List<String> = emptyList(),
onDismissRequest: () -> Unit,
onConfirm: (String) -> Unit
) {
var text by remember { mutableStateOf(initialValue) }
var text by remember(show) { mutableStateOf(initialValue) }
AlertDialog(
AppAlertDialog(
show = show,
onDismissRequest = onDismissRequest,
title = { AppText(title) },
text = {
title = title,
content = {
Column {
OutlinedTextField(
AppTextField(
value = text,
onValueChange = { text = it },
label = { AppText(hint) },
label = hint,
modifier = Modifier.fillMaxWidth(),
maxLines = 5
)
@@ -94,16 +95,13 @@ fun SourceInputDialog(
}
}
},
confirmButton = {
OutlinedButton(
onClick = { if (text.isNotBlank()) onConfirm(text) }
) { AppText(stringResource(android.R.string.ok)) }
confirmText = stringResource(android.R.string.ok),
onConfirm = {
// 拦截空输入,非空才执行回调
if (text.isNotBlank()) onConfirm(text)
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
AppText(stringResource(android.R.string.cancel))
}
}
dismissText = stringResource(android.R.string.cancel),
onDismiss = onDismissRequest
)
}
@@ -0,0 +1,39 @@
package io.legado.app.ui.widget.components.list
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@Composable
fun <T : Any> TopFloatingStickyItem(
item: T?,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + slideInVertically { -it },
exit: ExitTransition = fadeOut() + slideOutVertically { -it },
content: @Composable (T) -> Unit
) {
var lastItem by remember { mutableStateOf<T?>(null) }
AnimatedVisibility(
visible = item != null,
modifier = modifier,
enter = enter,
exit = exit
) {
item?.let { lastItem = it }
val currentItem = lastItem
if (currentItem != null) {
content(currentItem)
}
}
}
@@ -21,7 +21,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import top.yukonga.miuix.kmp.basic.ListPopupColumn
import top.yukonga.miuix.kmp.extra.SuperListPopup
import top.yukonga.miuix.kmp.overlay.OverlayListPopup
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -38,7 +38,7 @@ fun RoundDropdownMenu(
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
if (isMiuix) {
SuperListPopup(
OverlayListPopup(
show = expanded,
onDismissRequest = onDismissRequest,
popupModifier = modifier
@@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.rememberOpaqueColorScheme
import top.yukonga.miuix.kmp.extra.WindowBottomSheet
import top.yukonga.miuix.kmp.window.WindowBottomSheet
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -0,0 +1,90 @@
package io.legado.app.ui.widget.components.modalBottomSheet
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.text.AppText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OptionSheet(
show: Boolean,
onDismissRequest: () -> Unit,
title: String,
content: @Composable RowScope.() -> Unit
) {
AppModalBottomSheet(
show = show,
title = title,
onDismissRequest = onDismissRequest,
containerColor = LegadoTheme.colorScheme.surface
) {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
maxItemsInEachRow = 2
) {
content()
}
Spacer(modifier = Modifier.height(24.dp))
}
}
}
@Composable
fun RowScope.OptionCard(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
Surface(
onClick = onClick,
modifier = Modifier
.weight(1f)
.height(100.dp),
shape = MaterialTheme.shapes.medium,
color = LegadoTheme.colorScheme.surfaceContainerLow,
tonalElevation = 2.dp
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
AppIcon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = LegadoTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
AppText(
text = text,
style = LegadoTheme.typography.labelMedium,
maxLines = 1
)
}
}
}
@@ -17,7 +17,6 @@ import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -35,6 +34,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch
@@ -76,6 +78,45 @@ fun <T> RuleEditSheet(
fun getCurrentEntity() = fromFields(RuleEditFields(name, rule1, rule2), rule)
AppModalBottomSheet(
title = title,
startAction = {
IconButton(onClick = onDismissRequest) {
Icon(
Icons.Default.Close,
contentDescription = stringResource(R.string.cancel)
)
}
},
endAction = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem(
text = { AppText(stringResource(R.string.copy_rule)) },
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
onClick = {
onCopy(getCurrentEntity())
showMenu = false
}
)
DropdownMenuItem(
text = { AppText(stringResource(R.string.paste_rule)) },
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
scope.launch {
onPaste()?.let { pasted ->
val fields = toFields(pasted)
name = fields.name
rule1 = fields.rule1
rule2 = fields.rule2
}
}
showMenu = false
}
)
}
},
show = show,
onDismissRequest = onDismissRequest
) {
@@ -83,88 +124,39 @@ fun <T> RuleEditSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = 120.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
CenterAlignedTopAppBar(
title = { AppText(title) },
navigationIcon = {
IconButton(onClick = onDismissRequest) {
Icon(
Icons.Default.Close,
contentDescription = stringResource(R.string.cancel)
)
}
},
actions = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "More")
}
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem(
text = { AppText(stringResource(R.string.copy_rule)) },
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
onClick = {
onCopy(getCurrentEntity())
showMenu = false
}
)
DropdownMenuItem(
text = { AppText(stringResource(R.string.paste_rule)) },
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
onClick = {
scope.launch {
onPaste()?.let { pasted ->
val fields = toFields(pasted)
name = fields.name
rule1 = fields.rule1
rule2 = fields.rule2
}
}
showMenu = false
}
)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
AppTextField(
modifier = Modifier.fillMaxWidth(),
value = name,
onValueChange = { name = it },
label = stringResource(R.string.name),
singleLine = true
)
AppTextField(
modifier = Modifier.fillMaxWidth(),
value = rule1,
onValueChange = { rule1 = it },
label = label1
)
AppTextField(
modifier = Modifier.fillMaxWidth(),
value = rule2,
onValueChange = { rule2 = it },
label = label2,
minLines = 3
)
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = name,
onValueChange = { name = it },
label = { AppText(stringResource(R.string.name)) },
singleLine = true
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = rule1,
onValueChange = { rule1 = it },
label = { AppText(label1) }
)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = rule2,
onValueChange = { rule2 = it },
label = { AppText(label2) },
minLines = 3
)
}
}
FloatingActionButton(
AppFloatingActionButton(
onClick = { onSave(getCurrentEntity()) },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
containerColor = MaterialTheme.colorScheme.primaryContainer
.padding(16.dp)
) {
Icon(Icons.Default.Save, contentDescription = "Save")
AppIcon(Icons.Default.Save, contentDescription = "Save")
}
}
}
@@ -28,6 +28,7 @@ import io.legado.app.R
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.list.ListScaffold
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.ui.widget.components.text.AppText
@@ -71,31 +72,19 @@ fun <T> RuleListScaffold(
) {
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
if (showDeleteConfirmDialog) {
AlertDialog(
onDismissRequest = { showDeleteConfirmDialog = false },
title = { AppText(stringResource(R.string.delete)) },
text = { AppText(stringResource(R.string.del_msg)) },
confirmButton = {
OutlinedButton(
onClick = {
onDeleteSelected(state.selectedIds)
showDeleteConfirmDialog = false
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
AppText(stringResource(R.string.ok))
}
},
dismissButton = {
TextButton(onClick = { showDeleteConfirmDialog = false }) {
AppText(stringResource(R.string.cancel))
}
}
)
}
AppAlertDialog(
show = showDeleteConfirmDialog,
onDismissRequest = { showDeleteConfirmDialog = false },
title = stringResource(R.string.delete),
text = stringResource(R.string.del_msg),
confirmText = stringResource(R.string.ok),
onConfirm = {
onDeleteSelected(state.selectedIds)
showDeleteConfirmDialog = false
},
dismissText = stringResource(R.string.cancel),
onDismiss = { showDeleteConfirmDialog = false }
)
ListScaffold(
title = title,
@@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.preference.ArrowPreference
@Composable
fun ClickableSettingItem(
@@ -25,7 +25,7 @@ fun ClickableSettingItem(
) {
val composeEngine = LegadoTheme.composeEngine
if (ThemeResolver.isMiuixEngine(composeEngine)) {
SuperArrow(
ArrowPreference(
title = title,
summary = description,
insideMargin = BasicComponentDefaults.InsideMargin,
@@ -32,8 +32,8 @@ import io.legado.app.ui.widget.components.ValueStepper
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import top.yukonga.miuix.kmp.basic.BasicComponent
import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.extra.WindowDropdown
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.preference.WindowDropdownPreference
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -53,7 +53,7 @@ fun CompactDropdownSettingItem(
val selectedIndex = entryValues.indexOf(selectedValue).coerceAtLeast(0)
val spinnerItems = displayEntries.toList()
WindowDropdown(
WindowDropdownPreference(
title = title,
summary = description,
items = spinnerItems,
@@ -225,7 +225,7 @@ fun CompactSwitchSettingItem(
onCheckedChange: (Boolean) -> Unit
) {
if (ThemeResolver.isMiuixEngine(composeEngine)) {
SuperSwitch(
SwitchPreference(
title = title,
summary = description,
checked = checked,
@@ -12,7 +12,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import top.yukonga.miuix.kmp.basic.SpinnerEntry
import top.yukonga.miuix.kmp.extra.SuperSpinner
import top.yukonga.miuix.kmp.preference.OverlaySpinnerPreference
@Composable
fun DropdownListSettingItem(
@@ -32,7 +32,7 @@ fun DropdownListSettingItem(
SpinnerEntry(title = display)
}
SuperSpinner(
OverlaySpinnerPreference(
title = title,
summary = description,
items = spinnerItems,
@@ -6,7 +6,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.AdaptiveSwitch
import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.preference.SwitchPreference
@Composable
@@ -22,7 +22,7 @@ fun SwitchSettingItem(
val composeEngine = LegadoTheme.composeEngine
if (ThemeResolver.isMiuixEngine(composeEngine)) {
SuperSwitch(
SwitchPreference(
title = title,
summary = description,
checked = checked,
@@ -40,10 +40,8 @@ fun AppText(
) {
val baseStyle = style ?: LegadoTheme.typography.bodyMedium
// 2. 获取默认文本色:直接拿你自己封装的 LegadoTheme.colorScheme.onSurface
val defaultTextColor = LegadoTheme.colorScheme.onSurface
// 3. 颜色降级逻辑:传入的 color -> style 中的 color -> 主题默认色
val finalTextColor = color.takeOrElse {
baseStyle.color.takeOrElse { defaultTextColor }
}
@@ -20,9 +20,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -66,7 +67,7 @@ fun <T> DynamicTopAppBar(
},
navigationIcon = {
if (isSelecting || onBackClick != null) {
TopbarNavigationButton(
TopBarNavigationButton(
onClick = { if (isSelecting) onClearSelection() else onBackClick?.invoke() },
imageVector = if (isSelecting) AppIcons.Close else AppIcons.Back,
contentDescription = if (isSelecting) "取消选择" else "返回"
@@ -103,6 +104,8 @@ fun <T> DynamicTopAppBar(
scrollBehavior = scrollBehavior,
bottomContent = {
AnimatedVisibility(
modifier = Modifier
.adaptiveHorizontalPadding(),
visible = state.isSearch && !isSelecting,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
@@ -84,7 +84,7 @@ fun GlassMediumFlexibleTopAppBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
color = Color.Transparent, // 内部强制透明,透出 Column 背景
color = Color.Transparent,
scrollBehavior = (scrollBehavior as? MiuixGlassScrollBehavior)?.miuixBehavior
)
}
@@ -105,7 +105,7 @@ fun GlassMediumFlexibleTopAppBar(
navigationIcon = navigationIcon,
actions = actions,
scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
colors = transparentColors // 应用透明背景
colors = transparentColors
)
} else {
TopAppBar(
@@ -121,13 +121,12 @@ fun GlassMediumFlexibleTopAppBar(
navigationIcon = navigationIcon,
actions = actions,
scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior,
colors = transparentColors // 应用透明背景
colors = transparentColors
)
}
}
}
// 现在的 bottomContent 完美包裹在外层 Column 内,共享渐变和模糊效果!
bottomContent?.invoke(this)
}
}