[优化] 依旧修复一些Miuix主题的稳定性和界面问题,hmm议题依旧没时间修
This commit is contained in:
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+82
-95
@@ -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) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
+21
-37
@@ -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(
|
||||
|
||||
+44
-18
@@ -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(
|
||||
|
||||
+40
-109
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-17
@@ -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
|
||||
|
||||
+17
-19
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+3
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user