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

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