Procházet zdrojové kódy

[修复] 替换净化编辑或修改时无法正确更新正文的问题 #583
[新增] 替换净化编辑与管理界面使用Navigation处理导航逻辑与优化动画

HapeLee před 9 měsíci
rodič
revize
30dac8883e

+ 7 - 0
app/build.gradle.kts

@@ -6,6 +6,7 @@ plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.android.application)
     alias(libs.plugins.kotlin.android)
     alias(libs.plugins.kotlin.android)
     alias(libs.plugins.kotlin.parcelize)
     alias(libs.plugins.kotlin.parcelize)
+    alias(libs.plugins.kotlin.serialization)
     alias(libs.plugins.room)
     alias(libs.plugins.room)
     alias(libs.plugins.ksp)
     alias(libs.plugins.ksp)
     alias(libs.plugins.google.services)
     alias(libs.plugins.google.services)
@@ -181,6 +182,7 @@ dependencies {
     testImplementation(libs.junit)
     testImplementation(libs.junit)
     androidTestImplementation(libs.bundles.androidTest)
     androidTestImplementation(libs.bundles.androidTest)
     implementation(libs.kotlin.stdlib)
     implementation(libs.kotlin.stdlib)
+    implementation(libs.kotlinx.serialization.json)
     implementation(libs.bundles.coroutines)
     implementation(libs.bundles.coroutines)
     implementation(libs.core.ktx)
     implementation(libs.core.ktx)
     implementation(libs.appcompat.appcompat)
     implementation(libs.appcompat.appcompat)
@@ -256,6 +258,11 @@ dependencies {
     implementation(libs.androidx.compose.foundation)
     implementation(libs.androidx.compose.foundation)
     implementation(libs.androidx.constraintlayout.compose)
     implementation(libs.androidx.constraintlayout.compose)
     implementation(libs.androidx.compose.ui.viewbinding)
     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.navigation.compose)
     implementation(libs.androidx.compose.material)
     implementation(libs.androidx.compose.material)
     implementation(libs.compose.materialIcons)
     implementation(libs.compose.materialIcons)

+ 0 - 7
app/src/main/AndroidManifest.xml

@@ -211,13 +211,6 @@
             android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
             android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
             android:launchMode="singleTask"
             android:launchMode="singleTask"
             android:windowSoftInputMode="adjustResize|stateHidden" />
             android:windowSoftInputMode="adjustResize|stateHidden" />
-        <!-- 替换规则编辑 -->
-        <activity
-            android:name=".ui.replace.edit.ReplaceEditActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
-            android:launchMode="singleTask"
-            android:windowSoftInputMode="adjustResize|stateHidden" />
         <!-- 配置界面 -->
         <!-- 配置界面 -->
         <activity
         <activity
             android:name=".ui.config.ConfigActivity"
             android:name=".ui.config.ConfigActivity"

+ 8 - 1
app/src/main/java/io/legado/app/di/appModule.kt

@@ -13,6 +13,7 @@ import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.main.my.MyViewModel
 import io.legado.app.ui.main.my.MyViewModel
+import io.legado.app.ui.replace.ReplaceEditRoute
 import io.legado.app.ui.replace.ReplaceRuleViewModel
 import io.legado.app.ui.replace.ReplaceRuleViewModel
 import io.legado.app.ui.replace.edit.ReplaceEditViewModel
 import io.legado.app.ui.replace.edit.ReplaceEditViewModel
 import org.koin.android.ext.koin.androidApplication
 import org.koin.android.ext.koin.androidApplication
@@ -34,7 +35,13 @@ val appModule = module {
     single { SearchContentRepository() }
     single { SearchContentRepository() }
 
 
     viewModel { ReadRecordViewModel(get(), get()) }
     viewModel { ReadRecordViewModel(get(), get()) }
-    viewModel { ReplaceEditViewModel(get(), get(), get()) }
+    viewModel { (route: ReplaceEditRoute) ->
+        ReplaceEditViewModel(
+            app = get(),
+            replaceRuleDao = get(),
+            savedStateHandle = get()
+        )
+    }
     viewModel { ReplaceRuleViewModel(androidApplication()) }
     viewModel { ReplaceRuleViewModel(androidApplication()) }
     viewModel { ExploreShowViewModel(get()) }
     viewModel { ExploreShowViewModel(get()) }
     viewModel { SearchContentViewModel(get(), get()) }
     viewModel { SearchContentViewModel(get(), get()) }

+ 4 - 2
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.dialogs.alert
 //import io.legado.app.lib.theme.primaryColor
 //import io.legado.app.lib.theme.primaryColor
 import io.legado.app.model.ReadBook
 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.setLayout
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 
 
@@ -95,7 +96,8 @@ class EffectiveReplacesDialog : BaseDialogFragment(R.layout.dialog_recycler_view
                         showChineseConvertAlert()
                         showChineseConvertAlert()
                         return@let
                         return@let
                     }
                     }
-                    editActivity.launch(ReplaceEditActivity.startIntent(requireContext(), item.id))
+                    val editRoute = ReplaceEditRoute(id = item.id, pattern = item.pattern)
+                    ReplaceRuleActivity.startIntent(requireContext(), editRoute)
                 }
                 }
             }
             }
         }
         }

+ 16 - 13
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.browser.WebViewActivity
 import io.legado.app.ui.dict.DictDialog
 import io.legado.app.ui.dict.DictDialog
 import io.legado.app.ui.login.SourceLoginActivity
 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.ReplaceRuleActivity
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
 import io.legado.app.ui.widget.PopupAction
 import io.legado.app.ui.widget.PopupAction
 import io.legado.app.ui.widget.dialog.PhotoDialog
 import io.legado.app.ui.widget.dialog.PhotoDialog
 import io.legado.app.utils.Debounce
 import io.legado.app.utils.Debounce
@@ -981,20 +981,23 @@ class ReadBookActivity : BaseReadBookActivity(),
 
 
             R.id.menu_replace -> {
             R.id.menu_replace -> {
                 val scopes = arrayListOf<String>()
                 val scopes = arrayListOf<String>()
-                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 text = selectedText.lineSequence().map { it.trim() }.joinToString("\n")
+
+                val editRoute = ReplaceEditRoute(
+                    id = -1,
+                    pattern = text,
+                    scope = scopes.joinToString(";"),
+                    isScopeTitle = false,
+                    isScopeContent = true
+                )
+
                 replaceActivity.launch(
                 replaceActivity.launch(
-                    ReplaceEditActivity.startIntent(
-                        this,
-                        pattern = text,
-                        scope = scopes.joinToString(";"),
-                        isScopeTitle = false,
-                        isScopeContent = true
+                    ReplaceRuleActivity.startIntent(
+                        context = this,
+                        editRoute = editRoute
                     )
                     )
                 )
                 )
                 return true
                 return true

+ 11 - 15
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.text.style.StyleSpan
 import android.view.Menu
 import android.view.Menu
 import android.view.MenuItem
 import android.view.MenuItem
-import android.view.MotionEvent
 import android.view.View
 import android.view.View
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.viewModels
 import androidx.activity.viewModels
 import androidx.appcompat.widget.PopupMenu
 import androidx.appcompat.widget.PopupMenu
 import androidx.appcompat.widget.SearchView
 import androidx.appcompat.widget.SearchView
-import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentPagerAdapter
 import androidx.fragment.app.FragmentPagerAdapter
@@ -29,7 +27,6 @@ import io.legado.app.R
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.EventBus
 import io.legado.app.data.entities.Book
 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.ActivityChapterListBinding
 import io.legado.app.databinding.DialogDownloadChoiceBinding
 import io.legado.app.databinding.DialogDownloadChoiceBinding
 import io.legado.app.help.book.isLocal
 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.about.AppLogDialog
 import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
 import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
 import io.legado.app.ui.file.HandleFileContract
 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.ReplaceRuleActivity
-import io.legado.app.ui.replace.edit.ReplaceEditActivity
 import io.legado.app.ui.widget.dialog.WaitDialog
 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.gone
-import io.legado.app.utils.hideSoftInput
 import io.legado.app.utils.observeEvent
 import io.legado.app.utils.observeEvent
-import io.legado.app.utils.shouldHideSoftInput
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.visible
 import io.legado.app.utils.visible
@@ -262,14 +255,17 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>(),
                 val scopes = arrayListOf<String>()
                 val scopes = arrayListOf<String>()
                 viewModel.bookData.value?.name?.let { scopes.add(it) }
                 viewModel.bookData.value?.name?.let { scopes.add(it) }
                 viewModel.bookSource?.bookSourceUrl?.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(
                 replaceActivity.launch(
-                    ReplaceEditActivity.startIntent(
-                        this,
-                        pattern = "text",
-                        scope = scopes.joinToString(";"),
-                        isScopeTitle = true,
-                        isScopeContent = false
-                    )
+                    ReplaceRuleActivity.startIntent(this, editRoute)
                 )
                 )
                 return true
                 return true
             }
             }

+ 16 - 0
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
+)

+ 82 - 10
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt

@@ -1,23 +1,95 @@
 package io.legado.app.ui.replace
 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 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.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() {
 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
     @Composable
     override fun Content() {
     override fun Content() {
         AppTheme {
         AppTheme {
-            ReplaceRuleScreen(onBackClick = { finish() })
-        }
-    }
+            val navController = rememberNavController()
+            val context = LocalActivity.current
 
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-    }
+            LaunchedEffect(Unit) {
+                context?.setResult(RESULT_OK)
+            }
 
 
+            val startRouteJson = intent.getStringExtra(EXTRA_START_ROUTE)
+            val initialEditRoute = remember(startRouteJson) {
+                startRouteJson?.let { Json.decodeFromString<ReplaceEditRoute>(it) }
+            }
+
+            val startDestination: Any = initialEditRoute ?: ReplaceRuleRoute
+
+            SharedTransitionLayout {
+                NavHost(
+                    navController = navController,
+                    startDestination = startDestination
+                ) {
+                    composable<ReplaceRuleRoute> {
+                        ReplaceRuleScreen(
+                            sharedTransitionScope = this@SharedTransitionLayout,
+                            animatedVisibilityScope = this@composable,
+                            onBackClick = { finish() },
+                            onNavigateToEdit = { route -> navController.navigate(route) }
+                        )
+                    }
+
+                    composable<ReplaceEditRoute> { backStackEntry ->
+                        val route = backStackEntry.toRoute<ReplaceEditRoute>()
+                        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 = { /* ... */ }
+                        )
+                    }
+                }
+            }
+        }
+    }
 }
 }
+

+ 69 - 48
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.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
 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
@@ -86,7 +88,6 @@ import com.google.gson.Gson
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.data.repository.UploadRepository
 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.ActionItem
 import io.legado.app.ui.widget.components.AnimatedText
 import io.legado.app.ui.widget.components.AnimatedText
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
 import io.legado.app.ui.widget.components.DraggableSelectionHandler
@@ -111,8 +112,11 @@ 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,
 ) {
 ) {
 
 
     val uploadRepository: UploadRepository = koinInject()
     val uploadRepository: UploadRepository = koinInject()
@@ -519,16 +523,24 @@ fun ReplaceRuleScreen(
                 tooltip = { PlainTooltip { Text("Localized description") } },
                 tooltip = { PlainTooltip { Text("Localized description") } },
                 state = rememberTooltipState(),
                 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)
                     verticalArrangement = Arrangement.spacedBy(8.dp)
                 ) {
                 ) {
                     items(rules, key = { it.id }) { ui ->
                     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) {
                 if (inSelectionMode) {

+ 0 - 54
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditActivity.kt

@@ -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) {
-
-    }
-
-}

+ 230 - 186
app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt

@@ -1,6 +1,8 @@
 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.slideInVertically
 import androidx.compose.animation.slideInVertically
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
@@ -82,225 +84,267 @@ 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,
     onShowHelp: (String) -> Unit,
     onShowHelp: (String) -> 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 = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
     var showMenu by remember { mutableStateOf(false) }
     var showMenu by remember { mutableStateOf(false) }
     val isKeyboardVisible by keyboardAsState()
     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
                 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 = {
+
+                            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(
+                    modifier = Modifier
+                        .padding(paddingValues)
+                        .fillMaxSize()
+                        .verticalScroll(rememberScrollState())
+                        .imePadding()
+                        .padding(horizontal = 16.dp, vertical = 8.dp),
+                    verticalArrangement = Arrangement.spacedBy(16.dp)
+                ) {
 
 
-                GroupSelector(
-                    currentGroup = state.group,
-                    allGroups = state.allGroups,
-                    onGroupChange = viewModel::onGroupChange,
-                    onManageClick = { viewModel.toggleGroupDialog(true) }
-                )
+                    OutlinedTextField(
+                        value = state.name,
+                        onValueChange = viewModel::onNameChange,
+                        label = { Text("规则名称") },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onFocusChanged {
+                                if (it.isFocused) viewModel.activeField =
+                                    ReplaceEditViewModel.ActiveField.Name
+                            },
+                        singleLine = true
+                    )
 
 
-                OutlinedTextField(
-                    value = state.pattern,
-                    onValueChange = viewModel::onPatternChange,
-                    label = { Text("匹配规则") },
-                    placeholder = { Text("输入正则表达式或关键字") },
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .onFocusChanged {
-                            if (it.isFocused) viewModel.activeField =
-                                ReplaceEditViewModel.ActiveField.Pattern
-                        }
-                )
+                    GroupSelector(
+                        currentGroup = state.group,
+                        allGroups = state.allGroups,
+                        onGroupChange = viewModel::onGroupChange,
+                        onManageClick = { viewModel.toggleGroupDialog(true) }
+                    )
 
 
-                OutlinedTextField(
-                    value = state.replacement,
-                    onValueChange = viewModel::onReplacementChange,
-                    label = { Text("替换为") },
-                    placeholder = { Text("输入替换内容或捕获组") },
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .onFocusChanged {
-                            if (it.isFocused) viewModel.activeField =
-                                ReplaceEditViewModel.ActiveField.Replacement
-                        }
-                )
+                    OutlinedTextField(
+                        value = state.pattern,
+                        onValueChange = viewModel::onPatternChange,
+                        label = { Text("匹配规则") },
+                        placeholder = { Text("输入正则表达式或关键字") },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onFocusChanged {
+                                if (it.isFocused) viewModel.activeField =
+                                    ReplaceEditViewModel.ActiveField.Pattern
+                            }
+                    )
 
 
-                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
+                    OutlinedTextField(
+                        value = state.replacement,
+                        onValueChange = viewModel::onReplacementChange,
+                        label = { Text("替换为") },
+                        placeholder = { Text("输入替换内容或捕获组") },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onFocusChanged {
+                                if (it.isFocused) viewModel.activeField =
+                                    ReplaceEditViewModel.ActiveField.Replacement
+                            }
                     )
                     )
 
 
-                    Spacer(Modifier.width(8.dp))
+                    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
+                        )
+
+                    }
 
 
-                    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
+                    OutlinedTextField(
+                        value = state.scope,
+                        onValueChange = viewModel::onScopeChange,
+                        label = { Text("特定范围") },
+                        placeholder = { Text("指定规则适用的范围") },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onFocusChanged {
+                                if (it.isFocused) viewModel.activeField =
+                                    ReplaceEditViewModel.ActiveField.Scope
+                            }
                     )
                     )
 
 
-                    Spacer(Modifier.weight(1f))
+                    OutlinedTextField(
+                        value = state.excludeScope,
+                        onValueChange = viewModel::onExcludeScopeChange,
+                        label = { Text("排除范围") },
+                        placeholder = { Text("指定规则不适用的范围") },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .onFocusChanged {
+                                if (it.isFocused) viewModel.activeField =
+                                    ReplaceEditViewModel.ActiveField.Exclude
+                            }
+                    )
 
 
-                    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.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
-                        }
-                )
 
 
-                OutlinedTextField(
-                    value = state.timeout,
-                    onValueChange = viewModel::onTimeoutChange,
-                    label = { Text("超时 (ms)") },
-                    placeholder = { Text("3000") },
-                    modifier = Modifier.fillMaxWidth()
+            if (state.showGroupDialog) {
+                ManageGroupDialog(
+                    groups = state.allGroups.filter { it != "默认" },
+                    onDismiss = { viewModel.toggleGroupDialog(false) },
+                    onDelete = { viewModel.deleteGroups(it) }
                 )
                 )
-
-                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)
 @OptIn(ExperimentalMaterial3Api::class)

+ 18 - 8
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.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import androidx.navigation.toRoute
 import io.legado.app.data.dao.ReplaceRuleDao
 import io.legado.app.data.dao.ReplaceRuleDao
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
+import io.legado.app.ui.replace.ReplaceEditRoute
 import io.legado.app.utils.GSON
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getClipText
 import io.legado.app.utils.getClipText
 import io.legado.app.utils.sendToClip
 import io.legado.app.utils.sendToClip
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers
 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
 import kotlinx.coroutines.launch
 
 
 
 
 data class ReplaceEditUiState(
 data class ReplaceEditUiState(
-    val id: Long = System.currentTimeMillis(),
+    val id: Long = 0,
     val name: TextFieldValue = TextFieldValue(""),
     val name: TextFieldValue = TextFieldValue(""),
     val group: String = "默认",
     val group: String = "默认",
     val pattern: TextFieldValue = TextFieldValue(""),
     val pattern: TextFieldValue = TextFieldValue(""),
@@ -41,6 +46,8 @@ class ReplaceEditViewModel(
     private val savedStateHandle: SavedStateHandle
     private val savedStateHandle: SavedStateHandle
 ) : ViewModel() {
 ) : ViewModel() {
 
 
+    private val route = savedStateHandle.toRoute<ReplaceEditRoute>()
+
     private val _uiState = MutableStateFlow(ReplaceEditUiState())
     private val _uiState = MutableStateFlow(ReplaceEditUiState())
     val uiState = _uiState.asStateFlow()
     val uiState = _uiState.asStateFlow()
 
 
@@ -54,19 +61,22 @@ class ReplaceEditViewModel(
 
 
     private fun initData() {
     private fun initData() {
         viewModelScope.launch {
         viewModelScope.launch {
-            val id = savedStateHandle.get<Long>("id") ?: -1L
+
+            val id = route.id
+
             if (id > 0) {
             if (id > 0) {
                 val rule = replaceRuleDao.findById(id)
                 val rule = replaceRuleDao.findById(id)
                 rule?.let { updateStateFromRule(it) }
                 rule?.let { updateStateFromRule(it) }
             } else {
             } else {
                 _uiState.update {
                 _uiState.update {
                     it.copy(
                     it.copy(
+                        id = System.currentTimeMillis(),
                         name = TextFieldValue(""),
                         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(""),
                         excludeScope = TextFieldValue(""),
                     )
                     )
                 }
                 }

+ 1 - 0
build.gradle.kts

@@ -11,6 +11,7 @@ plugins {
     alias(libs.plugins.android.library) apply false
     alias(libs.plugins.android.library) apply false
     alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.kotlin.parcelize) 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.ksp) apply false
     alias(libs.plugins.google.services) apply false
     alias(libs.plugins.google.services) apply false
     alias(libs.plugins.room) apply false
     alias(libs.plugins.room) apply false

+ 17 - 5
gradle/libs.versions.toml

@@ -1,16 +1,21 @@
 [versions]
 [versions]
 
 
 accompanistWebview = "0.36.0"
 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"
 biometric = "1.2.0"
 coilCompose = "2.7.0"
 coilCompose = "2.7.0"
-composeBom = "2025.12.01"
+composeBom = "2026.01.00"
 constraintlayoutCompose = "1.1.1"
 constraintlayoutCompose = "1.1.1"
 coreSplashscreen = "1.2.0"
 coreSplashscreen = "1.2.0"
 datastorePreferences = "1.2.0"
 datastorePreferences = "1.2.0"
-foundation = "1.10.0"
+foundation = "1.10.1"
 kotlin = "2.3.0"
 kotlin = "2.3.0"
+kotlinxSerialization = "1.10.0"
 kotlinxCoroutinesAndroid = "1.10.2"
 kotlinxCoroutinesAndroid = "1.10.2"
+kotlinxSerializationJson = "1.10.0"
 ksp = "2.3.4"
 ksp = "2.3.4"
 agp = "8.13.2"
 agp = "8.13.2"
 appcompat = "1.7.1"
 appcompat = "1.7.1"
@@ -37,10 +42,11 @@ jsoupxpath = "2.5.3"
 coroutines = "1.10.2"
 coroutines = "1.10.2"
 lifecycleViewmodelCompose = "2.10.0"
 lifecycleViewmodelCompose = "2.10.0"
 liveeventbus = "1.8.14"
 liveeventbus = "1.8.14"
+navigation3 = "1.0.0"
 markdownCompose = "0.5.0"
 markdownCompose = "0.5.0"
 markwon = "4.6.2"
 markwon = "4.6.2"
 material = "1.14.0-alpha08"
 material = "1.14.0-alpha08"
-material3 = "1.5.0-alpha11"
+material3 = "1.5.0-alpha12"
 material3IconsExtended = "1.7.8"
 material3IconsExtended = "1.7.8"
 materialKolor = "4.1.0"
 materialKolor = "4.1.0"
 media = "1.7.1"
 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-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
 androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
 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-animation = { module = "androidx.compose.animation:animation", version.ref = "animation" }
 androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
 androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
 androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
 androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
 androidx-compose-material = { module = "androidx.compose.material:material" }
 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" }
 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-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
 androidx-compose-ui = { module = "androidx.compose.ui:ui" }
 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" }
 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-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" }
 libarchive = { module = "me.zhanghai.android.libarchive:library", version.ref = "libarchive" }
 lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
 lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "lifecycle" }
 lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", 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" }
 junit = { module = "junit:junit", version = "4.13.2" }
 kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
 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-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-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
 kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
 kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }