diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5f8ca08f5..bea2b2978 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.room)
alias(libs.plugins.ksp)
alias(libs.plugins.google.services)
@@ -181,6 +182,7 @@ dependencies {
testImplementation(libs.junit)
androidTestImplementation(libs.bundles.androidTest)
implementation(libs.kotlin.stdlib)
+ implementation(libs.kotlinx.serialization.json)
implementation(libs.bundles.coroutines)
implementation(libs.core.ktx)
implementation(libs.appcompat.appcompat)
@@ -256,6 +258,11 @@ dependencies {
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.compose.ui.viewbinding)
+ implementation(libs.androidx.navigation3.runtime)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(libs.androidx.compose.adaptive)
+ implementation(libs.androidx.compose.adaptive.layout)
+ implementation(libs.androidx.compose.adaptive.navigation)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.compose.material)
implementation(libs.compose.materialIcons)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 95dbdc1b0..b1a19d69a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -211,13 +211,6 @@
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize|stateHidden" />
-
-
+ ReplaceEditViewModel(
+ app = get(),
+ replaceRuleDao = get(),
+ savedStateHandle = get()
+ )
+ }
viewModel { ReplaceRuleViewModel(androidApplication()) }
viewModel { ExploreShowViewModel(get()) }
viewModel { SearchContentViewModel(get(), get()) }
diff --git a/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt
index 1230bfce0..5df512a0b 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/EffectiveReplacesDialog.kt
@@ -20,7 +20,8 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.model.ReadBook
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
+import io.legado.app.ui.replace.ReplaceEditRoute
+import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -95,7 +96,8 @@ class EffectiveReplacesDialog : BaseDialogFragment(R.layout.dialog_recycler_view
showChineseConvertAlert()
return@let
}
- editActivity.launch(ReplaceEditActivity.startIntent(requireContext(), item.id))
+ val editRoute = ReplaceEditRoute(id = item.id, pattern = item.pattern)
+ ReplaceRuleActivity.startIntent(requireContext(), editRoute)
}
}
}
diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
index 00938fad9..ee2876072 100644
--- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
@@ -102,8 +102,8 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.dict.DictDialog
import io.legado.app.ui.login.SourceLoginActivity
+import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleActivity
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.PopupAction
import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.Debounce
@@ -981,20 +981,23 @@ class ReadBookActivity : BaseReadBookActivity(),
R.id.menu_replace -> {
val scopes = arrayListOf()
- ReadBook.book?.name?.let {
- scopes.add(it)
- }
- ReadBook.bookSource?.bookSourceUrl?.let {
- scopes.add(it)
- }
+ ReadBook.book?.name?.let { scopes.add(it) }
+ ReadBook.bookSource?.bookSourceUrl?.let { scopes.add(it) }
+
val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n")
+
+ val editRoute = ReplaceEditRoute(
+ id = -1,
+ pattern = text,
+ scope = scopes.joinToString(";"),
+ isScopeTitle = false,
+ isScopeContent = true
+ )
+
replaceActivity.launch(
- ReplaceEditActivity.startIntent(
- this,
- pattern = text,
- scope = scopes.joinToString(";"),
- isScopeTitle = false,
- isScopeContent = true
+ ReplaceRuleActivity.startIntent(
+ context = this,
+ editRoute = editRoute
)
)
return true
diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
index 848d7f60d..6c9faa8b0 100644
--- a/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/book/toc/TocActivity.kt
@@ -14,13 +14,11 @@ import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.view.Menu
import android.view.MenuItem
-import android.view.MotionEvent
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
-import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentPagerAdapter
@@ -29,7 +27,6 @@ import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.EventBus
import io.legado.app.data.entities.Book
-import io.legado.app.data.entities.BookChapter
import io.legado.app.databinding.ActivityChapterListBinding
import io.legado.app.databinding.DialogDownloadChoiceBinding
import io.legado.app.help.book.isLocal
@@ -41,15 +38,11 @@ import io.legado.app.model.ReadBook
import io.legado.app.ui.about.AppLogDialog
import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
import io.legado.app.ui.file.HandleFileContract
+import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleActivity
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.dialog.WaitDialog
-import io.legado.app.utils.applyTint
-import io.legado.app.utils.dpToPx
import io.legado.app.utils.gone
-import io.legado.app.utils.hideSoftInput
import io.legado.app.utils.observeEvent
-import io.legado.app.utils.shouldHideSoftInput
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible
@@ -262,14 +255,17 @@ class TocActivity : VMBaseActivity(),
val scopes = arrayListOf()
viewModel.bookData.value?.name?.let { scopes.add(it) }
viewModel.bookSource?.bookSourceUrl?.let { scopes.add(it) }
+
+ val editRoute = ReplaceEditRoute(
+ id = -1,
+ pattern = "text",
+ scope = scopes.joinToString(";"),
+ isScopeTitle = true,
+ isScopeContent = false
+ )
+
replaceActivity.launch(
- ReplaceEditActivity.startIntent(
- this,
- pattern = "text",
- scope = scopes.joinToString(";"),
- isScopeTitle = true,
- isScopeContent = false
- )
+ ReplaceRuleActivity.startIntent(this, editRoute)
)
return true
}
diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt
new file mode 100644
index 000000000..6dc90c6c7
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceEditRoute.kt
@@ -0,0 +1,16 @@
+package io.legado.app.ui.replace
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+object ReplaceRuleRoute
+
+@Serializable
+data class ReplaceEditRoute(
+ val id: Long = -1,
+ val pattern: String? = null,
+ val isRegex: Boolean = false,
+ val scope: String? = null,
+ val isScopeTitle: Boolean = false,
+ val isScopeContent: Boolean = false
+)
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
index bc5d98460..507703dfa 100644
--- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
@@ -1,23 +1,95 @@
package io.legado.app.ui.replace
-import android.os.Bundle
-import androidx.activity.compose.setContent
-import androidx.appcompat.app.AppCompatActivity
+import android.content.Context
+import android.content.Intent
+import androidx.activity.compose.LocalActivity
+import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.runtime.Composable
-import io.legado.app.ui.theme.AppTheme
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.toRoute
import io.legado.app.base.BaseComposeActivity
+import io.legado.app.ui.replace.edit.ReplaceEditScreen
+import io.legado.app.ui.replace.edit.ReplaceEditViewModel
+import io.legado.app.ui.theme.AppTheme
+import kotlinx.serialization.json.Json
+import org.koin.androidx.compose.koinViewModel
+import org.koin.core.parameter.parametersOf
class ReplaceRuleActivity : BaseComposeActivity() {
+ companion object {
+ const val EXTRA_START_ROUTE = "start_route"
+ fun startIntent(
+ context: Context,
+ editRoute: ReplaceEditRoute? = null
+ ): Intent = Intent(context, ReplaceRuleActivity::class.java).apply {
+ editRoute?.let {
+ putExtra(EXTRA_START_ROUTE, Json.encodeToString(it))
+ }
+ }
+ }
+
@Composable
override fun Content() {
AppTheme {
- ReplaceRuleScreen(onBackClick = { finish() })
+ val navController = rememberNavController()
+ val context = LocalActivity.current
+
+ LaunchedEffect(Unit) {
+ context?.setResult(RESULT_OK)
+ }
+
+ val startRouteJson = intent.getStringExtra(EXTRA_START_ROUTE)
+ val initialEditRoute = remember(startRouteJson) {
+ startRouteJson?.let { Json.decodeFromString(it) }
+ }
+
+ val startDestination: Any = initialEditRoute ?: ReplaceRuleRoute
+
+ SharedTransitionLayout {
+ NavHost(
+ navController = navController,
+ startDestination = startDestination
+ ) {
+ composable {
+ ReplaceRuleScreen(
+ sharedTransitionScope = this@SharedTransitionLayout,
+ animatedVisibilityScope = this@composable,
+ onBackClick = { finish() },
+ onNavigateToEdit = { route -> navController.navigate(route) }
+ )
+ }
+
+ composable { backStackEntry ->
+ val route = backStackEntry.toRoute()
+ val viewModel: ReplaceEditViewModel = koinViewModel { parametersOf(route) }
+
+ ReplaceEditScreen(
+ sharedTransitionScope = this@SharedTransitionLayout,
+ animatedVisibilityScope = this@composable,
+ viewModel = viewModel,
+ onBack = {
+ // 如果编辑页是唯一的页面,点击返回应关闭 Activity
+ if (navController.previousBackStackEntry == null) {
+ finish()
+ } else {
+ navController.popBackStack()
+ }
+ },
+ onSaveSuccess = {
+ if (navController.previousBackStackEntry == null) finish()
+ else navController.popBackStack()
+ },
+ onShowHelp = { /* ... */ }
+ )
+ }
+ }
+ }
}
}
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- }
-
}
+
diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
index 700487730..e2111d093 100644
--- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
@@ -4,6 +4,8 @@ import android.content.ClipData
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
@@ -86,7 +88,6 @@ import com.google.gson.Gson
import io.legado.app.R
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.data.repository.UploadRepository
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AnimatedText
import io.legado.app.ui.widget.components.DraggableSelectionHandler
@@ -111,8 +112,11 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
)
@Composable
fun ReplaceRuleScreen(
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
viewModel: ReplaceRuleViewModel = koinViewModel(),
- onBackClick: () -> Unit
+ onBackClick: () -> Unit,
+ onNavigateToEdit: (ReplaceEditRoute) -> Unit,
) {
val uploadRepository: UploadRepository = koinInject()
@@ -519,16 +523,24 @@ fun ReplaceRuleScreen(
tooltip = { PlainTooltip { Text("Localized description") } },
state = rememberTooltipState(),
) {
- FloatingActionButton(
- modifier = Modifier.animateFloatingActionButton(
- visible = !inSelectionMode,
- alignment = Alignment.BottomEnd,
- ),
- onClick = {
- context.startActivity(ReplaceEditActivity.startIntent(context))
+ with(sharedTransitionScope) {
+ FloatingActionButton(
+ modifier = Modifier
+ .animateFloatingActionButton(
+ visible = !inSelectionMode,
+ alignment = Alignment.BottomEnd,
+ )
+ .sharedBounds(
+ sharedContentState = rememberSharedContentState(key = "fab_add"),
+ animatedVisibilityScope = animatedVisibilityScope,
+ resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds
+ ),
+ onClick = {
+ onNavigateToEdit(ReplaceEditRoute(id = -1))
+ }
+ ) {
+ Icon(Icons.Default.Add, contentDescription = "Add Rule")
}
- ) {
- Icon(Icons.Default.Add, contentDescription = "Add Rule")
}
}
}
@@ -555,44 +567,53 @@ fun ReplaceRuleScreen(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(rules, key = { it.id }) { ui ->
- ReorderableSelectionItem(
- state = reorderableState,
- key = ui.id,
- title = ui.name,
- isEnabled = ui.isEnabled,
- isSelected = selectedRuleIds.contains(ui.id),
- inSelectionMode = inSelectionMode,
- canReorder = canReorder,
- onToggleSelection = {
- viewModel.toggleSelection(ui.id)
- },
- onEnabledChange = { enabled ->
- viewModel.update(ui.rule.copy(isEnabled = enabled))
- },
- onClickEdit = {
- context.startActivity(
- ReplaceEditActivity.startIntent(
- context,
- ui.id
+ with(sharedTransitionScope) {
+ ReorderableSelectionItem(
+ state = reorderableState,
+ key = ui.id,
+ title = ui.name,
+ isEnabled = ui.isEnabled,
+ isSelected = selectedRuleIds.contains(ui.id),
+ inSelectionMode = inSelectionMode,
+ canReorder = canReorder,
+ onToggleSelection = {
+ viewModel.toggleSelection(ui.id)
+ },
+ onEnabledChange = { enabled ->
+ viewModel.update(ui.rule.copy(isEnabled = enabled))
+ },
+ onClickEdit = {
+ onNavigateToEdit(
+ ReplaceEditRoute(
+ id = ui.id,
+ pattern = ui.rule.pattern
+ )
)
- )
- },
- modifier = Modifier.padding(horizontal = 12.dp),
- dropdownContent = { dismiss ->
- DropdownMenuItem(
- text = { Text("移至顶部") },
- onClick = { viewModel.toTop(ui.rule); dismiss() }
- )
- DropdownMenuItem(
- text = { Text("移至底部") },
- onClick = { viewModel.toBottom(ui.rule); dismiss() }
- )
- DropdownMenuItem(
- text = { Text("删除") },
- onClick = { showDeleteRuleDialog = ui.rule; dismiss() }
- )
- }
- )
+ },
+ modifier = Modifier
+ .padding(horizontal = 12.dp)
+ .sharedBounds(
+ sharedContentState = rememberSharedContentState(key = "rule_${ui.id}"),
+ animatedVisibilityScope = animatedVisibilityScope,
+ resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
+ //clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(12.dp))
+ ),
+ dropdownContent = { dismiss ->
+ DropdownMenuItem(
+ text = { Text("移至顶部") },
+ onClick = { viewModel.toTop(ui.rule); dismiss() }
+ )
+ DropdownMenuItem(
+ text = { Text("移至底部") },
+ onClick = { viewModel.toBottom(ui.rule); dismiss() }
+ )
+ DropdownMenuItem(
+ text = { Text("删除") },
+ onClick = { showDeleteRuleDialog = ui.rule; dismiss() }
+ )
+ }
+ )
+ }
}
}
if (inSelectionMode) {
diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
deleted file mode 100644
index df5fa4a55..000000000
--- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package io.legado.app.ui.replace.edit
-
-import android.content.Context
-import android.content.Intent
-import androidx.activity.compose.setContent
-import androidx.compose.runtime.Composable
-import io.legado.app.ui.theme.AppTheme
-import io.legado.app.base.BaseComposeActivity
-
-/**
- * 编辑替换规则
- */
-class ReplaceEditActivity : BaseComposeActivity() {
-
- companion object {
- fun startIntent(
- context: Context,
- id: Long = -1,
- pattern: String? = null,
- isRegex: Boolean = false,
- scope: String? = null,
- isScopeTitle: Boolean = false,
- isScopeContent: Boolean = false
- ): Intent = Intent(context, ReplaceEditActivity::class.java).apply {
- putExtra("id", id)
- putExtra("pattern", pattern)
- putExtra("isRegex", isRegex)
- putExtra("scope", scope)
- putExtra("isScopeTitle", isScopeTitle)
- putExtra("isScopeContent", isScopeContent)
- }
- }
-
- @Composable
- override fun Content() {
- setContent {
- AppTheme {
- ReplaceEditScreen(
- onBack = { finish() },
- onSaveSuccess = {
- setResult(RESULT_OK)
- finish()
- },
- onShowHelp = { key -> showHelp(key) }
- )
- }
- }
- }
-
- private fun showHelp(key: String) {
-
- }
-
-}
diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
index c82a10977..f76895336 100644
--- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt
@@ -1,6 +1,8 @@
package io.legado.app.ui.replace.edit
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
@@ -82,225 +84,267 @@ fun keyboardAsState(): State {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ReplaceEditScreen(
+ sharedTransitionScope: SharedTransitionScope,
+ animatedVisibilityScope: AnimatedVisibilityScope,
onBack: () -> Unit,
onSaveSuccess: () -> Unit,
onShowHelp: (String) -> Unit,
viewModel: ReplaceEditViewModel = koinViewModel()
) {
val state by viewModel.uiState.collectAsState()
+ val sharedKey = remember(state.id) {
+ if (state.id > 0) "rule_${state.id}" else "fab_add"
+ }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showMenu by remember { mutableStateOf(false) }
val isKeyboardVisible by keyboardAsState()
- Box(modifier = Modifier.fillMaxSize()) {
- Scaffold(
- modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
- topBar = {
- GlassMediumFlexibleTopAppBar(
- title = { Text(if (state.id > 0) "编辑替换规则" else "新增替换规则") },
- navigationIcon = {
- IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") }
- },
- actions = {
- IconButton(onClick = { onShowHelp("regexHelp") }) {
- Icon(Icons.AutoMirrored.Filled.HelpOutline, contentDescription = "帮助")
- }
-
- 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 = { Text("复制规则") },
- onClick = {
- showMenu = false
- viewModel.copyRule()
- }
- )
-
- DropdownMenuItem(
- text = { Text("粘贴规则") },
- onClick = {
- showMenu = false
- viewModel.pasteRule(onSuccess = {})
- }
- )
- }
- },
- scrollBehavior = scrollBehavior
- )
- }
- ) { paddingValues ->
- Column(
+ with(sharedTransitionScope) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ Scaffold(
modifier = Modifier
- .padding(paddingValues)
- .fillMaxSize()
- .verticalScroll(rememberScrollState())
- .imePadding()
- .padding(horizontal = 16.dp, vertical = 8.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
-
- OutlinedTextField(
- value = state.name,
- onValueChange = viewModel::onNameChange,
- label = { Text("规则名称") },
- modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.isFocused) viewModel.activeField =
- ReplaceEditViewModel.ActiveField.Name
+ .nestedScroll(scrollBehavior.nestedScrollConnection)
+ .sharedBounds(
+ sharedContentState = rememberSharedContentState(key = sharedKey),
+ animatedVisibilityScope = animatedVisibilityScope,
+ resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
+ //clipInOverlayDuringTransition = OverlayClip(RoundedCornerShape(16.dp))
+ ),
+ topBar = {
+ GlassMediumFlexibleTopAppBar(
+ title = { Text(if (state.id > 0) "编辑替换规则" else "新增替换规则") },
+ navigationIcon = {
+ IconButton(onClick = onBack) {
+ Icon(
+ Icons.AutoMirrored.Filled.ArrowBack,
+ "Back"
+ )
+ }
},
- singleLine = true
- )
+ actions = {
- GroupSelector(
- currentGroup = state.group,
- allGroups = state.allGroups,
- onGroupChange = viewModel::onGroupChange,
- onManageClick = { viewModel.toggleGroupDialog(true) }
- )
+ IconButton(onClick = { onShowHelp("regexHelp") }) {
+ Icon(
+ Icons.AutoMirrored.Filled.HelpOutline,
+ contentDescription = "帮助"
+ )
+ }
- OutlinedTextField(
- value = state.pattern,
- onValueChange = viewModel::onPatternChange,
- label = { Text("匹配规则") },
- placeholder = { Text("输入正则表达式或关键字") },
+ 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 = { Text("复制规则") },
+ onClick = {
+ showMenu = false
+ viewModel.copyRule()
+ }
+ )
+
+ DropdownMenuItem(
+ text = { Text("粘贴规则") },
+ onClick = {
+ showMenu = false
+ viewModel.pasteRule(onSuccess = {})
+ }
+ )
+ }
+ },
+ scrollBehavior = scrollBehavior
+ )
+ }
+ ) { paddingValues ->
+ Column(
modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.isFocused) viewModel.activeField =
- ReplaceEditViewModel.ActiveField.Pattern
- }
- )
-
- OutlinedTextField(
- value = state.replacement,
- onValueChange = viewModel::onReplacementChange,
- label = { Text("替换为") },
- placeholder = { Text("输入替换内容或捕获组") },
- modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.isFocused) viewModel.activeField =
- ReplaceEditViewModel.ActiveField.Replacement
- }
- )
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Start
+ .padding(paddingValues)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .imePadding()
+ .padding(horizontal = 16.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
) {
- FilterChip(
- selected = state.scopeTitle,
- onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
- label = { Text("标题") },
- leadingIcon = if (state.scopeTitle) {
- { Icon(Icons.Default.Check, contentDescription = "已选", Modifier.size(FilterChipDefaults.IconSize)) }
- } else null
+
+ OutlinedTextField(
+ value = state.name,
+ onValueChange = viewModel::onNameChange,
+ label = { Text("规则名称") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged {
+ if (it.isFocused) viewModel.activeField =
+ ReplaceEditViewModel.ActiveField.Name
+ },
+ singleLine = true
)
- Spacer(Modifier.width(8.dp))
-
- FilterChip(
- selected = state.scopeContent,
- onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
- label = { Text("内容") },
- leadingIcon = if (state.scopeContent) {
- { Icon(Icons.Default.Check, contentDescription = "已选", Modifier.size(FilterChipDefaults.IconSize)) }
- } else null
+ GroupSelector(
+ currentGroup = state.group,
+ allGroups = state.allGroups,
+ onGroupChange = viewModel::onGroupChange,
+ onManageClick = { viewModel.toggleGroupDialog(true) }
)
- Spacer(Modifier.weight(1f))
-
- FilterChip(
- selected = state.isRegex,
- onClick = { viewModel.onRegexChange(!state.isRegex) },
- label = { Text("使用正则") },
- leadingIcon = if (state.isRegex) {
- { Icon(Icons.Default.Check, contentDescription = "正则已启用", Modifier.size(FilterChipDefaults.IconSize)) }
- } else null
+ OutlinedTextField(
+ value = state.pattern,
+ onValueChange = viewModel::onPatternChange,
+ label = { Text("匹配规则") },
+ placeholder = { Text("输入正则表达式或关键字") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged {
+ if (it.isFocused) viewModel.activeField =
+ ReplaceEditViewModel.ActiveField.Pattern
+ }
)
+ OutlinedTextField(
+ value = state.replacement,
+ onValueChange = viewModel::onReplacementChange,
+ label = { Text("替换为") },
+ placeholder = { Text("输入替换内容或捕获组") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged {
+ if (it.isFocused) viewModel.activeField =
+ ReplaceEditViewModel.ActiveField.Replacement
+ }
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Start
+ ) {
+ FilterChip(
+ selected = state.scopeTitle,
+ onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
+ label = { Text("标题") },
+ leadingIcon = if (state.scopeTitle) {
+ {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = "已选",
+ Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else null
+ )
+
+ Spacer(Modifier.width(8.dp))
+
+ FilterChip(
+ selected = state.scopeContent,
+ onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
+ label = { Text("内容") },
+ leadingIcon = if (state.scopeContent) {
+ {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = "已选",
+ Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else null
+ )
+
+ Spacer(Modifier.weight(1f))
+
+ FilterChip(
+ selected = state.isRegex,
+ onClick = { viewModel.onRegexChange(!state.isRegex) },
+ label = { Text("使用正则") },
+ leadingIcon = if (state.isRegex) {
+ {
+ Icon(
+ Icons.Default.Check,
+ contentDescription = "正则已启用",
+ Modifier.size(FilterChipDefaults.IconSize)
+ )
+ }
+ } else null
+ )
+
+ }
+
+ OutlinedTextField(
+ value = state.scope,
+ onValueChange = viewModel::onScopeChange,
+ label = { Text("特定范围") },
+ placeholder = { Text("指定规则适用的范围") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged {
+ if (it.isFocused) viewModel.activeField =
+ ReplaceEditViewModel.ActiveField.Scope
+ }
+ )
+
+ OutlinedTextField(
+ value = state.excludeScope,
+ onValueChange = viewModel::onExcludeScopeChange,
+ label = { Text("排除范围") },
+ placeholder = { Text("指定规则不适用的范围") },
+ modifier = Modifier
+ .fillMaxWidth()
+ .onFocusChanged {
+ if (it.isFocused) viewModel.activeField =
+ ReplaceEditViewModel.ActiveField.Exclude
+ }
+ )
+
+ OutlinedTextField(
+ value = state.timeout,
+ onValueChange = viewModel::onTimeoutChange,
+ label = { Text("超时 (ms)") },
+ placeholder = { Text("3000") },
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Spacer(Modifier.height(16.dp))
+
}
+ }
- OutlinedTextField(
- value = state.scope,
- onValueChange = viewModel::onScopeChange,
- label = { Text("特定范围") },
- placeholder = { Text("指定规则适用的范围") },
- modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.isFocused) viewModel.activeField =
- ReplaceEditViewModel.ActiveField.Scope
- }
+ AnimatedVisibility(
+ visible = isKeyboardVisible,
+ enter = slideInVertically(
+ initialOffsetY = { fullHeight -> fullHeight },
+ ),
+ exit = slideOutVertically(
+ targetOffsetY = { fullHeight -> fullHeight },
+ ),
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .imePadding()
+ ) {
+ QuickInputBar(
+ onInsert = { text -> viewModel.insertTextAtCursor(text) }
)
+ }
- OutlinedTextField(
- value = state.excludeScope,
- onValueChange = viewModel::onExcludeScopeChange,
- label = { Text("排除范围") },
- placeholder = { Text("指定规则不适用的范围") },
- modifier = Modifier
- .fillMaxWidth()
- .onFocusChanged {
- if (it.isFocused) viewModel.activeField =
- ReplaceEditViewModel.ActiveField.Exclude
- }
+
+ if (state.showGroupDialog) {
+ ManageGroupDialog(
+ groups = state.allGroups.filter { it != "默认" },
+ onDismiss = { viewModel.toggleGroupDialog(false) },
+ onDelete = { viewModel.deleteGroups(it) }
)
-
- OutlinedTextField(
- value = state.timeout,
- onValueChange = viewModel::onTimeoutChange,
- label = { Text("超时 (ms)") },
- placeholder = { Text("3000") },
- modifier = Modifier.fillMaxWidth()
- )
-
- Spacer(Modifier.height(16.dp))
-
}
}
-
- AnimatedVisibility(
- visible = isKeyboardVisible,
- enter = slideInVertically(
- initialOffsetY = { fullHeight -> fullHeight },
- ),
- exit = slideOutVertically(
- targetOffsetY = { fullHeight -> fullHeight },
- ),
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .imePadding()
- ) {
- QuickInputBar(
- onInsert = { text -> viewModel.insertTextAtCursor(text) }
- )
- }
-
-
- if (state.showGroupDialog) {
- ManageGroupDialog(
- groups = state.allGroups.filter { it != "默认" },
- onDismiss = { viewModel.toggleGroupDialog(false) },
- onDelete = { viewModel.deleteGroups(it) }
- )
- }
}
+
}
@OptIn(ExperimentalMaterial3Api::class)
diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt
index 05ae1042d..aa3b7fe9b 100644
--- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditViewModel.kt
@@ -6,21 +6,26 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
import io.legado.app.data.dao.ReplaceRuleDao
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.exception.NoStackTraceException
+import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getClipText
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
data class ReplaceEditUiState(
- val id: Long = System.currentTimeMillis(),
+ val id: Long = 0,
val name: TextFieldValue = TextFieldValue(""),
val group: String = "默认",
val pattern: TextFieldValue = TextFieldValue(""),
@@ -41,6 +46,8 @@ class ReplaceEditViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
+ private val route = savedStateHandle.toRoute()
+
private val _uiState = MutableStateFlow(ReplaceEditUiState())
val uiState = _uiState.asStateFlow()
@@ -54,19 +61,22 @@ class ReplaceEditViewModel(
private fun initData() {
viewModelScope.launch {
- val id = savedStateHandle.get("id") ?: -1L
+
+ val id = route.id
+
if (id > 0) {
val rule = replaceRuleDao.findById(id)
rule?.let { updateStateFromRule(it) }
} else {
_uiState.update {
it.copy(
+ id = System.currentTimeMillis(),
name = TextFieldValue(""),
- pattern = TextFieldValue(savedStateHandle["pattern"] ?: ""),
- isRegex = savedStateHandle["isRegex"] ?: false,
- scope = TextFieldValue(savedStateHandle["scope"] ?: ""),
- scopeTitle = savedStateHandle["isScopeTitle"] ?: false,
- scopeContent = savedStateHandle["isScopeContent"] ?: false,
+ pattern = TextFieldValue(route.pattern ?: ""),
+ isRegex = route.isRegex,
+ scope = TextFieldValue(route.scope ?: ""),
+ scopeTitle = route.isScopeTitle,
+ scopeContent = route.isScopeContent,
excludeScope = TextFieldValue(""),
)
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 268349fc4..ca6a55bf9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -11,6 +11,7 @@ plugins {
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.parcelize) apply false
+ alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.room) apply false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ec9ea2924..900d47da5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,16 +1,21 @@
[versions]
accompanistWebview = "0.36.0"
-animation = "1.10.0"
+adaptive = "1.3.0-alpha06"
+adaptiveLayout = "1.3.0-alpha06"
+adaptiveNavigation = "1.3.0-alpha06"
+animation = "1.10.1"
biometric = "1.2.0"
coilCompose = "2.7.0"
-composeBom = "2025.12.01"
+composeBom = "2026.01.00"
constraintlayoutCompose = "1.1.1"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.0"
-foundation = "1.10.0"
+foundation = "1.10.1"
kotlin = "2.3.0"
+kotlinxSerialization = "1.10.0"
kotlinxCoroutinesAndroid = "1.10.2"
+kotlinxSerializationJson = "1.10.0"
ksp = "2.3.4"
agp = "8.13.2"
appcompat = "1.7.1"
@@ -37,10 +42,11 @@ jsoupxpath = "2.5.3"
coroutines = "1.10.2"
lifecycleViewmodelCompose = "2.10.0"
liveeventbus = "1.8.14"
+navigation3 = "1.0.0"
markdownCompose = "0.5.0"
markwon = "4.6.2"
material = "1.14.0-alpha08"
-material3 = "1.5.0-alpha11"
+material3 = "1.5.0-alpha12"
material3IconsExtended = "1.7.8"
materialKolor = "4.1.0"
media = "1.7.1"
@@ -89,10 +95,15 @@ androidx-annotation = { group = "androidx.annotation", name = "annotation", vers
androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
+androidx-compose-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" }
+androidx-compose-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" }
+androidx-compose-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "adaptiveNavigation" }
androidx-compose-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
androidx-compose-material = { module = "androidx.compose.material:material" }
+androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" }
+androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
compose-materialIcons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "material3IconsExtended" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
@@ -138,6 +149,7 @@ androidx-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
hutool-crypto = { module = "cn.hutool:hutool-crypto", version.ref = "hutool" }
kotlinx-coroutines-android-v181 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
+kotlinx-serialization-json-v163 = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
libarchive = { module = "me.zhanghai.android.libarchive:library", version.ref = "libarchive" }
lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
@@ -206,7 +218,7 @@ jsoupxpath = { module = "cn.wanghaomiao:JsoupXpath", version.ref = "jsoupxpath"
junit = { module = "junit:junit", version = "4.13.2" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
-
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }