HapeLee 5 달 전
부모
커밋
be2041c7da

+ 2 - 1
.claude/settings.local.json

@@ -101,7 +101,8 @@
       "Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
       "Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
       "Bash(Select-Object Name)",
       "Bash(Select-Object Name)",
       "PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })",
       "PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })",
-      "Bash(gh pr *)"
+      "Bash(gh pr *)",
+      "PowerShell(Get-Content *)"
     ]
     ]
   }
   }
 }
 }

+ 7 - 4
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt

@@ -8,8 +8,9 @@ import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.CircleShape
@@ -63,9 +64,8 @@ fun BookshelfConfigSheet(
             modifier = Modifier
             modifier = Modifier
                 .fillMaxWidth()
                 .fillMaxWidth()
                 .verticalScroll(rememberScrollState())
                 .verticalScroll(rememberScrollState())
-                .animateContentSize()
-                .padding(bottom = 32.dp),
-            verticalArrangement = Arrangement.spacedBy(8.dp)
+                .animateContentSize(),
+            verticalArrangement = Arrangement.spacedBy(8.dp),
         ) {
         ) {
             CompactDropdownSettingItem(
             CompactDropdownSettingItem(
                 title = stringResource(R.string.group_style),
                 title = stringResource(R.string.group_style),
@@ -506,6 +506,9 @@ fun BookshelfConfigSheet(
                 steps = 100,
                 steps = 100,
                 onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
                 onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
             )
             )
+
+            Spacer(modifier = Modifier.height(32.dp))
+
         }
         }
 
 
         LabelColorManageSheet(
         LabelColorManageSheet(

+ 31 - 0
app/src/main/java/io/legado/app/ui/main/homepage/HomepageContract.kt

@@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.domain.model.HomepageModuleType
 import io.legado.app.domain.model.HomepageModuleType
+import io.legado.app.domain.model.ModuleDef
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.persistentListOf
 
 
@@ -13,6 +14,36 @@ data class HomepageUiState(
     val isManageMode: Boolean = false,
     val isManageMode: Boolean = false,
     val isConfigMode: Boolean = false,
     val isConfigMode: Boolean = false,
     val isRefreshing: Boolean = false,
     val isRefreshing: Boolean = false,
+    val manageState: HomepageManageUiState = HomepageManageUiState()
+)
+
+@Stable
+data class HomepageManageUiState(
+    val sets: ImmutableList<HomepageSourceManageUi> = persistentListOf(),
+    val browseSources: ImmutableList<HomepageSourceManageUi> = persistentListOf(),
+    val allJoinedModules: ImmutableList<HomepageModuleManageUi> = persistentListOf(),
+    val sourceNames: Map<String, String> = emptyMap()
+)
+
+@Stable
+data class HomepageManageActions(
+    val onToggleSet: (String, Boolean) -> Unit = { _, _ -> },
+    val onGetSourceModules: (String, String?) -> List<HomepageModuleManageUi> = { _, _ -> emptyList() },
+    val onSyncSourceModules: (String) -> Unit = {},
+    val onToggleModule: (String, Boolean) -> Unit = { _, _ -> },
+    val onJoinModule: (String, String?, ModuleDef) -> Unit = { _, _, _ -> },
+    val onAddCustomModule: (String, String?, ModuleDef) -> Unit = { _, _, _ -> },
+    val onAddButtonGroupFromKinds: (String, String?, String, List<String>) -> Unit = { _, _, _, _ -> },
+    val onGetExploreKinds: (String) -> List<Pair<String, String>> = { emptyList() },
+    val onUpdateModule: (String, ModuleDef) -> Unit = { _, _ -> },
+    val onDeleteModule: (String) -> Unit = {},
+    val onReorderModules: (List<String>) -> Unit = {},
+    val onReorderSets: (List<String>) -> Unit = {},
+    val onSetCustomSetTitle: (String, String?) -> Unit = { _, _ -> },
+    val onCreateCustomSet: (String) -> Unit = {},
+    val onRenameCustomSet: (String, String) -> Unit = { _, _ -> },
+    val onDeleteCustomSet: (String) -> Unit = {},
+    val onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
 )
 )
 
 
 @Stable
 @Stable

+ 317 - 922
app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt

@@ -1,93 +1,56 @@
 package io.legado.app.ui.main.homepage
 package io.legado.app.ui.main.homepage
 
 
-import androidx.compose.foundation.layout.Arrangement
+import androidx.activity.compose.PredictiveBackHandler
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.material.icons.filled.DriveFileRenameOutline
-import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.FilterList
 import androidx.compose.material.icons.filled.FilterList
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import com.google.gson.JsonParser
 import io.legado.app.R
 import io.legado.app.R
-import io.legado.app.domain.model.HomepageModuleType
-import io.legado.app.domain.model.ModuleDef
-import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.main.homepage.manage.AddCustomModuleDialog
+import io.legado.app.ui.main.homepage.manage.AddDialogPrefill
+import io.legado.app.ui.main.homepage.manage.BrowseSourcesPage
+import io.legado.app.ui.main.homepage.manage.CustomSetAddModulesPage
+import io.legado.app.ui.main.homepage.manage.SetDetailPage
+import io.legado.app.ui.main.homepage.manage.SetListPage
+import io.legado.app.ui.main.homepage.manage.SourceBrowseDetailPage
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.JsonConfigEditor
-import io.legado.app.ui.widget.components.JsonRawEditor
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.SecondaryButton
 import io.legado.app.ui.widget.components.button.SmallIconButton
 import io.legado.app.ui.widget.components.button.SmallIconButton
-import io.legado.app.ui.widget.components.card.GlassCard
-import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
-import io.legado.app.ui.widget.components.card.SelectionItemCard
-import io.legado.app.ui.widget.components.divider.PillDivider
-import io.legado.app.ui.widget.components.divider.PillHeaderDivider
-import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
-import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
-import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
-import io.legado.app.ui.widget.components.tabRow.AppTabRow
-import io.legado.app.ui.widget.components.text.AppText
-import io.legado.app.utils.move
-import sh.calvin.reorderable.rememberReorderableLazyListState
+import kotlinx.collections.immutable.toImmutableList
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun <T> HomepageModuleManageSheet(
 fun <T> HomepageModuleManageSheet(
     data: T?,
     data: T?,
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
-    sets: List<HomepageSourceManageUi>,
-    browseSources: List<HomepageSourceManageUi>,
-    onToggleSet: (String, Boolean) -> Unit,
-    onGetModulesInSet: (String) -> List<HomepageModuleManageUi>,
-    onGetSourceModules: (String, String?) -> List<HomepageModuleManageUi>,
-    onToggleModule: (String, Boolean) -> Unit,
-    onJoinModule: (String, String?, ModuleDef) -> Unit,
-    onAddCustomModule: (String, String?, ModuleDef) -> Unit,
-    onAddButtonGroupFromKinds: (String, String?, String, List<String>) -> Unit,
-    onGetExploreKinds: (String) -> List<Pair<String, String>>,
-    onUpdateModule: (String, ModuleDef) -> Unit,
-    onDeleteModule: (String) -> Unit,
-    onReorderModules: (List<String>) -> Unit,
-    onReorderSets: (List<String>) -> Unit = {},
-    onSetCustomSetTitle: (String, String?) -> Unit,
-    onCreateCustomSet: (String) -> Unit,
-    onRenameCustomSet: (String, String) -> Unit,
-    onDeleteCustomSet: (String) -> Unit,
-    onGetAllModulesGroupedBySource: () -> Map<String, List<HomepageModuleManageUi>> = { emptyMap() },
-    onGetSourceName: (String) -> String = { it },
-    onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
-    onSyncSourceModules: (String) -> Unit = {},
+    state: HomepageManageUiState,
+    actions: HomepageManageActions,
 ) {
 ) {
     var selectingSetUrl by remember(data != null) { mutableStateOf<String?>(null) }
     var selectingSetUrl by remember(data != null) { mutableStateOf<String?>(null) }
     var browsingSourceUrl by remember(data != null) { mutableStateOf<String?>(null) }
     var browsingSourceUrl by remember(data != null) { mutableStateOf<String?>(null) }
@@ -106,23 +69,38 @@ fun <T> HomepageModuleManageSheet(
     var browseModuleType by remember(data != null) { mutableStateOf("card") }
     var browseModuleType by remember(data != null) { mutableStateOf("card") }
     var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
     var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
     var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
     var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
-    var showKindSelect by remember(data != null) { mutableStateOf(false) }
     var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) }
     var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) }
     val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions)
     val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions)
     var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) }
     var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) }
 
 
-    val currentTargetSetId = remember(selectingSetUrl) {
-        selectingSetUrl?.let { HomepageViewModel.customSetIdFromUrl(it) }
+    val effectiveTargetSetId = remember(selectingSetUrl, browsingSourceUrl) {
+        val url =
+            selectingSetUrl ?: browsingSourceUrl?.let { HomepageViewModel.customSetUrl("src_$it") }
+        url?.let { HomepageViewModel.customSetIdFromUrl(it) }
     }
     }
 
 
-    val allGroups = remember(browseSources) {
-        browseSources.flatMap { it.sourceGroup?.split(",") ?: emptyList() }
+    val infiniteModuleIdInTargetSet = remember(state.allJoinedModules, effectiveTargetSetId) {
+        state.allJoinedModules.find {
+            it.customSetId == effectiveTargetSetId && HomepageViewModel.isInfinite(
+                it.type,
+                it.layoutConfig
+            )
+        }?.id
+    }
+
+    val canSelectInfiniteGlobal = infiniteModuleIdInTargetSet == null
+
+    val allGroups = remember(state.browseSources) {
+        state.browseSources.flatMap { it.sourceGroup?.split(",") ?: emptyList() }
             .filter { it.isNotBlank() }.distinct().sorted()
             .filter { it.isNotBlank() }.distinct().sorted()
     }
     }
 
 
-    val filteredBrowseSources = remember(browseSources, groupFilter) {
-        if (groupFilter == null) browseSources
-        else browseSources.filter { it.sourceGroup?.split(",")?.contains(groupFilter) == true }
+    val filteredBrowseSources = remember(state.browseSources, groupFilter) {
+        val list = if (groupFilter == null) state.browseSources
+        else state.browseSources.filter {
+            it.sourceGroup?.split(",")?.contains(groupFilter) == true
+        }
+        list.toImmutableList()
     }
     }
 
 
     AppModalBottomSheet(
     AppModalBottomSheet(
@@ -137,12 +115,12 @@ fun <T> HomepageModuleManageSheet(
         title = when {
         title = when {
             showCustomSetAddModules -> stringResource(R.string.homepage_add_module)
             showCustomSetAddModules -> stringResource(R.string.homepage_add_module)
             browsingSourceUrl != null && browsingDetail ->
             browsingSourceUrl != null && browsingDetail ->
-                browseSources.find { it.sourceUrl == browsingSourceUrl }?.sourceName
+                state.browseSources.find { it.sourceUrl == browsingSourceUrl }?.sourceName
                     ?: stringResource(R.string.homepage_module_list)
                     ?: stringResource(R.string.homepage_module_list)
 
 
             showSourceBrowser || browsingSourceUrl != null -> stringResource(R.string.homepage_browse_source_modules)
             showSourceBrowser || browsingSourceUrl != null -> stringResource(R.string.homepage_browse_source_modules)
             selectingSetUrl != null && HomepageViewModel.isCustomSetUrl(selectingSetUrl!!) ->
             selectingSetUrl != null && HomepageViewModel.isCustomSetUrl(selectingSetUrl!!) ->
-                (sets.find { it.sourceUrl == selectingSetUrl }?.sourceName
+                (state.sets.find { it.sourceUrl == selectingSetUrl }?.sourceName
                     ?: stringResource(R.string.homepage_set_detail))
                     ?: stringResource(R.string.homepage_set_detail))
 
 
             else -> stringResource(R.string.homepage_module_manage)
             else -> stringResource(R.string.homepage_module_manage)
@@ -206,896 +184,313 @@ fun <T> HomepageModuleManageSheet(
             }
             }
         }
         }
     ) {
     ) {
+        var backProgress by remember { mutableFloatStateOf(0f) }
+        PredictiveBackHandler(
+            enabled = showCustomSetAddModules || browsingDetail || showSourceBrowser || browsingSourceUrl != null || selectingSetUrl != null
+        ) { progress ->
+            try {
+                progress.collect { event ->
+                    backProgress = event.progress
+                }
+                when {
+                    showCustomSetAddModules -> showCustomSetAddModules = false
+                    browsingDetail -> browsingDetail = false
+                    showSourceBrowser -> showSourceBrowser = false
+                    browsingSourceUrl != null -> browsingSourceUrl = null
+                    selectingSetUrl != null -> selectingSetUrl = null
+                }
+            } finally {
+                backProgress = 0f
+            }
+        }
+
         val setUrl = selectingSetUrl
         val setUrl = selectingSetUrl
         val browseUrl = browsingSourceUrl
         val browseUrl = browsingSourceUrl
         val isBrowsing = showSourceBrowser || browseUrl != null
         val isBrowsing = showSourceBrowser || browseUrl != null
 
 
         LaunchedEffect(browseUrl) {
         LaunchedEffect(browseUrl) {
-            browseUrl?.let { onSyncSourceModules(it) }
+            browseUrl?.let { actions.onSyncSourceModules(it) }
         }
         }
 
 
-        when {
-            browseUrl != null && browsingDetail -> {
-                // 三级:浏览书源的模块列表(已加入 / 书源模块 / 发现)
-                val displaySetUrl =
-                    selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
-                val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
-                val joinedModules = remember(displaySetUrl, sets, browseSources) {
-                    onGetModulesInSet(displaySetUrl).distinctBy { it.id }
-                }
+        val currentLevel = when {
+            showCustomSetAddModules || (browseUrl != null && browsingDetail) -> 2
+            isBrowsing || (setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl)) -> 1
+            else -> 0
+        }
 
 
-                val standardModules = remember(joinedModules) {
-                    joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
-                }
-                val infiniteModules = remember(joinedModules) {
-                    joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+        AnimatedContent(
+            targetState = currentLevel to (showCustomSetAddModules to browsingDetail),
+            transitionSpec = {
+                if (targetState.first > initialState.first) {
+                    (slideInHorizontally { it } + fadeIn()).togetherWith(slideOutHorizontally { -it / 3 } + fadeOut())
+                } else {
+                    (slideInHorizontally { -it / 3 } + fadeIn()).togetherWith(slideOutHorizontally { it } + fadeOut())
                 }
                 }
-
-                val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
-                val sourceModules = onGetSourceModules(browseUrl, currentSetId)
-                val exploreKinds = remember(browseUrl) { onGetExploreKinds(browseUrl) }
-
-                Column {
-                    AppTabRow(
-                        tabTitles = listOf(
-                            stringResource(R.string.homepage_tab_joined),
-                            stringResource(R.string.homepage_tab_source_modules),
-                            stringResource(R.string.homepage_tab_discover)
-                        ),
-                        selectedTabIndex = browseTab,
-                        onTabSelected = { browseTab = it }
+            },
+            modifier = Modifier.graphicsLayer {
+                val scale = 1f - (backProgress * 0.08f)
+                scaleX = scale
+                scaleY = scale
+                alpha = 1f - (backProgress * 0.1f)
+            },
+            label = "PageTransition"
+        ) { _ ->
+            when {
+                browseUrl != null && browsingDetail -> {
+                    SourceBrowseDetailPage(
+                        browseUrl = browseUrl,
+                        selectingSetUrl = selectingSetUrl,
+                        allJoinedModules = state.allJoinedModules,
+                        canSelectInfiniteGlobal = canSelectInfiniteGlobal,
+                        onGetSourceModules = actions.onGetSourceModules,
+                        onGetExploreKinds = actions.onGetExploreKinds,
+                        onToggleModule = actions.onToggleModule,
+                        onJoinModule = actions.onJoinModule,
+                        onReorderModules = actions.onReorderModules,
+                        onEditModule = { editingModule = it },
+                        onRequestDeleteModule = { deleteConfirmId = it },
+                        onAddDialogPrefill = { addDialogPrefill = it },
+                        onShowAddButtonGroupDialog = { showAddButtonGroupDialog = true },
+                        browseTab = browseTab,
+                        onBrowseTabChange = { browseTab = it },
+                        browseModuleType = browseModuleType,
+                        onBrowseModuleTypeChange = { browseModuleType = it },
+                        selectedKindTitles = selectedKindTitles,
+                        onSelectedKindTitlesChange = { selectedKindTitles = it },
                     )
                     )
-                    when (browseTab) {
-                        0 -> {
-                            if (joinedModules.isEmpty()) {
-                                Box(
-                                    modifier = Modifier
-                                        .fillMaxWidth()
-                                        .padding(24.dp),
-                                    contentAlignment = Alignment.Center
-                                ) {
-                                    AppText(stringResource(R.string.homepage_no_joined_modules))
-                                }
-                            } else {
-                                var listData by remember(displaySetUrl, standardModules) {
-                                    mutableStateOf(
-                                        standardModules.distinctBy { it.id }
-                                    )
-                                }
-                                val listState = rememberLazyListState()
-                                val reorderableState =
-                                    rememberReorderableLazyListState(listState) { from, to ->
-                                        listData = listData.toMutableList().apply {
-                                            val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
-                                            val toIndex = (to.index - 1).coerceIn(0, lastIndex)
-                                            move(fromIndex, toIndex)
-                                        }
-                                    }
-                                LaunchedEffect(standardModules) {
-                                    if (!reorderableState.isAnyItemDragging) listData =
-                                        standardModules.distinctBy { it.id }
-                                }
-                                LaunchedEffect(reorderableState.isAnyItemDragging) {
-                                    if (!reorderableState.isAnyItemDragging) {
-                                        val orderedIds =
-                                            (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
-                                        if (orderedIds != joinedModules.map { it.id }) {
-                                            onReorderModules(orderedIds)
-                                        }
-                                    }
-                                }
-                                LazyColumn(
-                                    state = listState,
-                                    modifier = Modifier.fillMaxWidth(),
-                                    verticalArrangement = Arrangement.spacedBy(8.dp)
-                                ) {
-                                    item(key = "header_standard") {
-                                        AppText(
-                                            text = stringResource(R.string.homepage_standard_modules_sortable),
-                                            style = LegadoTheme.typography.labelMedium,
-                                            color = LegadoTheme.colorScheme.primary,
-                                            modifier = Modifier.padding(
-                                                horizontal = 16.dp,
-                                                vertical = 4.dp
-                                            )
-                                        )
-                                    }
-
-                                    items(listData, key = { it.id }) { module ->
-                                        ReorderableSelectionItem(
-                                            state = reorderableState,
-                                            key = module.id,
-                                            title = module.title,
-                                            subtitle = HomepageModuleType.fromKey(module.type).title,
-                                            isEnabled = module.isVisible,
-                                            containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                            onEnabledChange = { enabled ->
-                                                onToggleModule(module.id, enabled)
-                                                listData = listData.map {
-                                                    if (it.id == module.id) it.copy(isVisible = enabled) else it
-                                                }
-                                            },
-                                            trailingAction = {
-                                                SmallIconButton(
-                                                    onClick = { editingModule = module },
-                                                    imageVector = Icons.Default.Edit
-                                                )
-                                                SmallIconButton(
-                                                    onClick = { deleteConfirmId = module.id },
-                                                    imageVector = Icons.Default.Delete
-                                                )
-                                            }
-                                        )
-                                    }
-
-                                    if (infiniteModules.isNotEmpty()) {
-                                        item(key = "header_infinite") {
-                                            PillDivider(
-                                                modifier = Modifier.padding(
-                                                    vertical = 8.dp,
-                                                    horizontal = 16.dp
-                                                )
-                                            )
-                                            AppText(
-                                                text = stringResource(R.string.homepage_infinite_module_bottom),
-                                                style = LegadoTheme.typography.labelMedium,
-                                                modifier = Modifier.padding(
-                                                    horizontal = 16.dp,
-                                                    vertical = 4.dp
-                                                )
-                                            )
-                                        }
-
-                                        items(infiniteModules, key = { it.id }) { module ->
-                                            val isEffective =
-                                                infiniteModules.firstOrNull() == module
-                                            SelectionItemCard(
-                                                title = module.title,
-                                                subtitle = HomepageModuleType.fromKey(module.type).title + if (isEffective) stringResource(
-                                                    R.string.homepage_status_in_effect
-                                                ) else stringResource(R.string.homepage_status_blocked),
-                                                isEnabled = module.isVisible,
-                                                containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
-                                                onEnabledChange = { onToggleModule(module.id, it) },
-                                                trailingAction = {
-                                                    SmallIconButton(
-                                                        onClick = { editingModule = module },
-                                                        imageVector = Icons.Default.Edit
-                                                    )
-                                                    SmallIconButton(
-                                                        onClick = { deleteConfirmId = module.id },
-                                                        imageVector = Icons.Default.Delete
-                                                    )
-                                                }
-                                            )
-                                        }
-                                    }
-                                }
-                            }
-                        }
-
-                        1 -> {
-                            if (sourceModules.isEmpty()) {
-                                Box(
-                                    modifier = Modifier
-                                        .fillMaxWidth()
-                                        .padding(24.dp),
-                                    contentAlignment = Alignment.Center
-                                ) {
-                                    AppText(stringResource(R.string.homepage_source_json_empty))
-                                }
-                            } else {
-                                LazyColumn(
-                                    modifier = Modifier.fillMaxWidth(),
-                                    verticalArrangement = Arrangement.spacedBy(4.dp)
-                                ) {
-                                    items(
-                                        sourceModules.distinctBy { it.id },
-                                        key = { it.id }) { module ->
-                                        val isJoined = joinedKeys.contains(module.moduleKey)
-                                        SelectionItemCard(
-                                            title = module.title,
-                                            subtitle = module.moduleKey + if (isJoined) stringResource(
-                                                R.string.homepage_status_joined
-                                            ) else "",
-                                            containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                            isSelected = isJoined,
-                                            inSelectionMode = true,
-                                            onToggleSelection = {
-                                                if (!isJoined) onJoinModule(
-                                                    browseUrl, currentTargetSetId, ModuleDef(
-                                                        key = module.moduleKey,
-                                                        type = module.type,
-                                                        title = module.title,
-                                                        sourceUrl = browseUrl,
-                                                    )
-                                                )
-                                            }
-                                        )
-                                    }
-                                }
-                            }
-                        }
-
-                        2 -> {
-                            val isButtonGroup = browseModuleType == "buttonGroup"
-                            Column {
-                                val typeList = remember {
-                                    HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
-                                }
-
-                                GlassCard(
-                                    containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                    cornerRadius = 12.dp
-                                ) {
-                                    CompactDropdownSettingItem(
-                                        title = stringResource(R.string.homepage_module_type),
-                                        selectedValue = browseModuleType,
-                                        displayEntries = typeList.map { it.title }.toTypedArray(),
-                                        entryValues = typeList.map { it.key }.toTypedArray(),
-                                        onValueChange = {
-                                            browseModuleType = it; selectedKindTitles = emptySet()
-                                        }
-                                    )
-                                }
-
-                                Spacer(modifier = Modifier.height(16.dp))
-
-                                SelectionItemCard(
-                                    title = stringResource(R.string.homepage_select_from_kinds),
-                                    subtitle = if (isButtonGroup) {
-                                        if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds)
-                                        else stringResource(
-                                            R.string.homepage_n_selected,
-                                            selectedKindTitles.size
-                                        )
-                                    } else {
-                                        stringResource(R.string.homepage_select_one_kind)
-                                    },
-                                    containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                    onToggleSelection = { showKindSelect = true },
-                                    trailingAction = {
-                                        if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
-                                            SmallIconButton(
-                                                onClick = { showAddButtonGroupDialog = true },
-                                                imageVector = Icons.Default.Check
-                                            )
-                                        }
-                                    }
-                                )
-
-                                ExploreKindSelectSheet(
-                                    show = showKindSelect,
-                                    onDismissRequest = { showKindSelect = false },
-                                    sourceUrl = browseUrl,
-                                    multiple = isButtonGroup,
-                                    initialSelectedTitles = selectedKindTitles.toList(),
-                                    onSelected = { kinds ->
-                                        if (isButtonGroup) {
-                                            selectedKindTitles = kinds.map { it.title }.toSet()
-                                        } else {
-                                            kinds.firstOrNull()?.let { kind ->
-                                                addDialogPrefill = AddDialogPrefill(
-                                                    title = kind.title,
-                                                    url = kind.url ?: "",
-                                                    type = browseModuleType
-                                                )
-                                            }
-                                        }
-                                    }
-                                )
-
-                                PillDivider(
-                                    modifier = Modifier.padding(vertical = 12.dp)
-                                )
-
-                                SecondaryButton(
-                                    text = stringResource(R.string.homepage_manual_add),
-                                    onClick = {
-                                        addDialogPrefill = AddDialogPrefill(type = browseModuleType)
-                                    },
-                                    modifier = Modifier.fillMaxWidth()
-                                )
-                            }
-                        }
-                    }
                 }
                 }
-            }
 
 
-            showCustomSetAddModules -> {
-                // 自定义集添加模块
-                val currentSetUrl = selectingSetUrl!!
-                val currentSetId = HomepageViewModel.customSetIdFromUrl(currentSetUrl)
-                val initialJoined = onGetModulesInSet(currentSetUrl)
-                    .associateBy({ it.moduleKey }, { it.id })
-                var joinedInCurrent by remember(initialJoined) { mutableStateOf(initialJoined) }
-
-                val grouped = onGetAllModulesGroupedBySource()
-                LazyColumn(
-                    modifier = Modifier.fillMaxWidth(),
-                    verticalArrangement = Arrangement.spacedBy(8.dp)
-                ) {
-                    grouped.forEach { (sourceUrl, modules) ->
-                        item(key = "header_$sourceUrl") {
-                            AppText(
-                                text = onGetSourceName(sourceUrl),
-                                style = LegadoTheme.typography.labelLarge,
-                                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
-                            )
-                        }
-                        items(modules, key = { it.id }) { module ->
-                            val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
-                            val inCurrentSet = instanceIdInCurrentSet != null
-                            SelectionItemCard(
-                                title = module.title,
-                                subtitle = module.moduleKey,
-                                containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                isSelected = inCurrentSet,
-                                inSelectionMode = true,
-                                onToggleSelection = {
-                                    if (inCurrentSet) {
-                                        onDeleteModule(instanceIdInCurrentSet!!)
-                                        joinedInCurrent = joinedInCurrent - module.moduleKey
-                                    } else {
-                                        onAssignModuleToCustomSet(module.id, currentSetId)
-                                        joinedInCurrent =
-                                            joinedInCurrent + (module.moduleKey to "temp_${module.id}")
-                                    }
+                showCustomSetAddModules -> {
+                    CustomSetAddModulesPage(
+                        setUrl = selectingSetUrl!!,
+                        allJoinedModules = state.allJoinedModules,
+                        sourceNames = state.sourceNames,
+                        onToggleModuleToSet = { module, inCurrentSet, isBlocked ->
+                            if (inCurrentSet) {
+                                val instanceId = state.allJoinedModules
+                                    .find {
+                                        it.customSetId == HomepageViewModel.customSetIdFromUrl(
+                                            selectingSetUrl!!
+                                        ) && it.moduleKey == module.moduleKey
+                                    }?.id
+                                if (instanceId != null) {
+                                    actions.onDeleteModule(instanceId)
                                 }
                                 }
-                            )
-                        }
-                    }
-                }
-            }
-
-            isBrowsing -> {
-                // 二级:浏览书源列表
-                val sources = remember(filteredBrowseSources) {
-                    filteredBrowseSources.distinctBy { it.sourceUrl }
-                }
-                LazyColumn(
-                    modifier = Modifier.fillMaxWidth(),
-                    verticalArrangement = Arrangement.spacedBy(8.dp)
-                ) {
-                    items(sources, key = { it.sourceUrl }) { source ->
-                        val moduleCount = onGetSourceModules(source.sourceUrl, null).size
-                        SelectionItemCard(
-                            title = source.sourceName,
-                            subtitle = stringResource(R.string.homepage_n_modules, moduleCount),
-                            containerColor = LegadoTheme.colorScheme.onSheetContent,
-                            onToggleSelection = {
-                                browsingSourceUrl = source.sourceUrl
-                                browsingDetail = true
-                            }
-                        )
-                    }
-                }
-            }
-
-            setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl) -> {
-                // 二级:集详情
-                val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
-                val modules = remember(setUrl, sets) {
-                    onGetModulesInSet(setUrl).distinctBy { it.id }
-                }
-
-                val standardModules = remember(modules) {
-                    modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
-                }
-                val infiniteModules = remember(modules) {
-                    modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
-                }
-
-                if (modules.isEmpty()) {
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(24.dp),
-                        horizontalAlignment = Alignment.CenterHorizontally
-                    ) {
-                        AppText(stringResource(R.string.homepage_no_modules))
-                        SecondaryButton(
-                            text = stringResource(R.string.homepage_browse_to_add),
-                            onClick = {
-                                if (setId.startsWith("src_")) {
-                                    browsingSourceUrl = setId.removePrefix("src_")
-                                    browsingDetail = true
-                                } else {
-                                    showCustomSetAddModules = true
-                                }
-                            }
-                        )
-                    }
-                } else {
-                    var listData by remember(setUrl, standardModules) {
-                        mutableStateOf(standardModules)
-                    }
-                    val listState = rememberLazyListState()
-                    val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
-                        listData = listData.toMutableList().apply {
-                            val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
-                            val toIndex = (to.index - 1).coerceIn(0, lastIndex)
-                            move(fromIndex, toIndex)
-                        }
-                    }
-
-                    LaunchedEffect(standardModules) {
-                        if (!reorderableState.isAnyItemDragging) listData =
-                            standardModules
-                    }
-                    LaunchedEffect(reorderableState.isAnyItemDragging) {
-                        if (!reorderableState.isAnyItemDragging) {
-                            val orderedIds =
-                                (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
-                            if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
-                        }
-                    }
-
-                    LazyColumn(
-                        state = listState,
-                        modifier = Modifier.fillMaxWidth(),
-                        verticalArrangement = Arrangement.spacedBy(8.dp)
-                    ) {
-                        if (listData.isNotEmpty()) {
-                            item(key = "header_std_detail") {
-                                AppText(
-                                    text = stringResource(R.string.homepage_standard_module),
-                                    style = LegadoTheme.typography.labelMedium,
-                                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
-                                )
-                            }
-                            items(listData, key = { it.id }) { module ->
-                                ReorderableSelectionItem(
-                                    state = reorderableState,
-                                    key = module.id,
-                                    title = module.title,
-                                    subtitle = HomepageModuleType.fromKey(module.type).title,
-                                    isEnabled = module.isVisible,
-                                    containerColor = LegadoTheme.colorScheme.onSheetContent,
-                                    onEnabledChange = { enabled ->
-                                        onToggleModule(module.id, enabled)
-                                        listData = listData.map {
-                                            if (it.id == module.id) it.copy(isVisible = enabled) else it
-                                        }
-                                    },
-                                    trailingAction = {
-                                        SmallIconButton(
-                                            onClick = { editingModule = module },
-                                            imageVector = Icons.Default.Edit
-                                        )
-                                        SmallIconButton(
-                                            onClick = { deleteConfirmId = module.id },
-                                            imageVector = Icons.Default.Delete
-                                        )
-                                    }
-                                )
-                            }
-                        }
-
-                        if (infiniteModules.isNotEmpty()) {
-                            item(key = "header_inf_detail") {
-                                PillDivider()
-                                AppText(
-                                    text = stringResource(R.string.homepage_infinite_module_slot),
-                                    style = LegadoTheme.typography.labelMedium,
-                                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
-                                )
-                            }
-                            items(infiniteModules, key = { it.id }) { module ->
-                                val isEffective = infiniteModules.firstOrNull() == module
-                                SelectionItemCard(
-                                    title = module.title,
-                                    subtitle = HomepageModuleType.fromKey(module.type).title,
-                                    isEnabled = module.isVisible,
-                                    containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
-                                    onEnabledChange = { onToggleModule(module.id, it) },
-                                    trailingAction = {
-                                        SmallIconButton(
-                                            onClick = { editingModule = module },
-                                            imageVector = Icons.Default.Edit
-                                        )
-                                        SmallIconButton(
-                                            onClick = { deleteConfirmId = module.id },
-                                            imageVector = Icons.Default.Delete
-                                        )
-                                    }
+                            } else if (!isBlocked) {
+                                actions.onAssignModuleToCustomSet(
+                                    module.id,
+                                    HomepageViewModel.customSetIdFromUrl(selectingSetUrl!!)
                                 )
                                 )
                             }
                             }
-                        }
-
-                        item(key = "browse_from_set") {
-                            SecondaryButton(
-                                text = stringResource(R.string.homepage_browse_source_modules),
-                                onClick = {
-                                    if (setId.startsWith("src_")) {
-                                        browsingSourceUrl = setId.removePrefix("src_")
-                                        browsingDetail = true
-                                    } else {
-                                        showCustomSetAddModules = true
-                                    }
-                                },
-                                modifier = Modifier.fillMaxWidth()
-                            )
-                        }
-                    }
+                        },
+                    )
                 }
                 }
-            }
 
 
-            else -> {
-                // 一级:集列表
-                var localSets by remember(data != null, sets) {
-                    mutableStateOf(sets.distinctBy { it.sourceUrl })
+                isBrowsing -> {
+                    BrowseSourcesPage(
+                        browseSources = filteredBrowseSources,
+                        getSourceModules = actions.onGetSourceModules,
+                        onSelectSource = {
+                            browsingSourceUrl = it
+                            browsingDetail = true
+                        },
+                    )
                 }
                 }
-                val setsListState = rememberLazyListState()
-                val setsReorderableState =
-                    rememberReorderableLazyListState(setsListState) { from, to ->
-                        localSets = localSets.toMutableList().apply {
-                            val fromIndex = from.index.coerceIn(0, lastIndex)
-                            val toIndex = to.index.coerceIn(0, lastIndex)
-                            move(fromIndex, toIndex)
-                        }
-                    }
 
 
-                LaunchedEffect(sets) {
-                    if (!setsReorderableState.isAnyItemDragging) localSets = sets
-                }
-                LaunchedEffect(setsReorderableState.isAnyItemDragging) {
-                    if (!setsReorderableState.isAnyItemDragging) {
-                        val orderedUrls = localSets.map { it.sourceUrl }
-                        if (orderedUrls != sets.map { it.sourceUrl }) onReorderSets(orderedUrls)
-                    }
+                setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl) -> {
+                    SetDetailPage(
+                        setUrl = setUrl,
+                        allJoinedModules = state.allJoinedModules,
+                        onToggleModule = actions.onToggleModule,
+                        onReorderModules = actions.onReorderModules,
+                        onEditModule = { editingModule = it },
+                        onRequestDeleteModule = { deleteConfirmId = it },
+                        onBrowseSourceModules = {
+                            val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
+                            browsingSourceUrl = setId.removePrefix("src_")
+                            browsingDetail = true
+                        },
+                        onAddModules = {
+                            showCustomSetAddModules = true
+                        },
+                    )
                 }
                 }
 
 
-                LazyColumn(
-                    state = setsListState,
-                    modifier = Modifier.fillMaxWidth(),
-                    verticalArrangement = Arrangement.spacedBy(8.dp)
-                ) {
-                    items(localSets, key = { it.sourceUrl }) { set ->
-                        ReorderableSelectionItem(
-                            state = setsReorderableState,
-                            key = set.sourceUrl,
-                            title = set.sourceName,
-                            subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
-                            containerColor = LegadoTheme.colorScheme.onSheetContent,
-                            isEnabled = set.isSelected,
-                            onToggleSelection = { selectingSetUrl = set.sourceUrl },
-                            onEnabledChange = { enabled ->
-                                onToggleSet(set.sourceUrl, enabled)
-                                localSets = localSets.map {
-                                    if (it.sourceUrl == set.sourceUrl) it.copy(isSelected = enabled) else it
-                                }
-                            },
-                            trailingAction = {
-                                SmallIconButton(
-                                    onClick = { renameSetId = set.sourceUrl },
-                                    imageVector = Icons.Default.DriveFileRenameOutline
-                                )
-                                SmallIconButton(
-                                    onClick = { deleteSetConfirmId = set.sourceUrl },
-                                    imageVector = Icons.Default.Delete
-                                )
-                            }
-                        )
-                    }
-                    item {
-                        PillDivider(
-                            modifier = Modifier.padding(vertical = 12.dp)
-                        )
-                    }
-                    item(key = "create_set") {
-                        SecondaryButton(
-                            text = stringResource(R.string.homepage_new_custom_set),
-                            onClick = { showCreateSetDialog = true },
-                            modifier = Modifier.fillMaxWidth()
-                        )
-                    }
-                    item(key = "browse_sources") {
-                        SecondaryButton(
-                            text = stringResource(R.string.homepage_browse_source_modules),
-                            onClick = { showSourceBrowser = true },
-                            modifier = Modifier.fillMaxWidth()
-                        )
-                    }
+                else -> {
+                    SetListPage(
+                        sets = state.sets,
+                        onToggleSet = actions.onToggleSet,
+                        onReorderSets = actions.onReorderSets,
+                        onSelectSet = { selectingSetUrl = it },
+                        onRenameSet = { renameSetId = it },
+                        onDeleteSet = { deleteSetConfirmId = it },
+                        onCreateSet = { showCreateSetDialog = true },
+                        onBrowseSources = { showSourceBrowser = true },
+                    )
                 }
                 }
             }
             }
         }
         }
-    }
-
-    var tempName by remember { mutableStateOf("") }
-    AppAlertDialog(
-        data = renameSetId,
-        onDismissRequest = { renameSetId = null },
-        title = stringResource(R.string.homepage_rename_custom_set),
-        content = { setId ->
-            val currentName =
-                remember(setId) { sets.find { it.sourceUrl == setId }?.sourceName ?: "" }
-            LaunchedEffect(setId) { tempName = currentName }
-            AppTextField(
-                value = tempName,
-                onValueChange = { tempName = it },
-                label = stringResource(R.string.homepage_name_label),
-                modifier = Modifier.fillMaxWidth()
-            )
-        },
-        onConfirm = { setId ->
-            if (tempName.isNotBlank()) onRenameCustomSet(
-                HomepageViewModel.customSetIdFromUrl(setId),
-                tempName
-            )
-            renameSetId = null
-        },
-        confirmText = stringResource(R.string.dialog_confirm),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { renameSetId = null }
-    )
-
-    AppAlertDialog(
-        data = if (showCreateSetDialog) Unit else null,
-        onDismissRequest = { showCreateSetDialog = false },
-        title = stringResource(R.string.homepage_new_custom_set_title),
-        content = {
-            LaunchedEffect(Unit) { tempName = "" }
-            AppTextField(
-                value = tempName,
-                onValueChange = { tempName = it },
-                label = stringResource(R.string.homepage_name_label),
-                modifier = Modifier.fillMaxWidth()
-            )
-        },
-        onConfirm = {
-            if (tempName.isNotBlank()) onCreateCustomSet(tempName)
-            showCreateSetDialog = false
-        },
-        confirmText = stringResource(R.string.dialog_confirm),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { showCreateSetDialog = false }
-    )
-
-    AppAlertDialog(
-        data = deleteSetConfirmId,
-        onDismissRequest = { deleteSetConfirmId = null },
-        title = stringResource(R.string.homepage_delete_custom_set),
-        text = stringResource(R.string.homepage_delete_custom_set_confirm),
-        onConfirm = { setId ->
-            onDeleteCustomSet(HomepageViewModel.customSetIdFromUrl(setId))
-            deleteSetConfirmId = null
-        },
-        confirmText = stringResource(R.string.delete),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { deleteSetConfirmId = null }
-    )
-
-    AppAlertDialog(
-        data = deleteConfirmId,
-        onDismissRequest = { deleteConfirmId = null },
-        title = stringResource(R.string.homepage_remove_module),
-        text = stringResource(R.string.homepage_remove_module_confirm),
-        onConfirm = { id ->
-            onDeleteModule(id)
-            deleteConfirmId = null
-        },
-        confirmText = stringResource(R.string.remove),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { deleteConfirmId = null }
-    )
-
-    AddCustomModuleDialog(
-        data = addDialogPrefill,
-        sourceUrl = browsingSourceUrl ?: "",
-        targetSetId = currentTargetSetId ?: "",
-        prefillTitle = addDialogPrefill?.title ?: "",
-        prefillUrl = addDialogPrefill?.url ?: "",
-        prefillType = addDialogPrefill?.type ?: "card",
-        onDismissRequest = { addDialogPrefill = null },
-        onConfirm = { def ->
-            onAddCustomModule(browsingSourceUrl!!, currentTargetSetId, def)
-            addDialogPrefill = null
-        }
-    )
-
-    AddCustomModuleDialog(
-        data = editingModule,
-        sourceUrl = editingModule?.sourceUrl ?: "",
-        targetSetId = editingModule?.customSetId ?: "",
-        prefillTitle = editingModule?.title ?: "",
-        prefillUrl = editingModule?.url ?: "",
-        prefillType = editingModule?.type ?: "card",
-        prefillArgs = editingModule?.args ?: "",
-        prefillLayoutConfig = editingModule?.layoutConfig ?: "",
-        onDismissRequest = { editingModule = null },
-        onConfirm = { def ->
-            onUpdateModule(editingModule!!.id, def)
-            editingModule = null
-        }
-    )
-
-    var titleState by remember { mutableStateOf("") }
-    AppAlertDialog(
-        data = if (showAddButtonGroupDialog) Unit else null,
-        onDismissRequest = { showAddButtonGroupDialog = false },
-        title = stringResource(R.string.homepage_add_button_group),
-        content = {
-            val quickActionsLabel = stringResource(R.string.homepage_quick_actions)
-            LaunchedEffect(Unit) { tempButtonGroupTitle = quickActionsLabel }
-            AppTextField(
-                value = tempButtonGroupTitle,
-                onValueChange = { tempButtonGroupTitle = it },
-                label = stringResource(R.string.homepage_module_title_label),
-                modifier = Modifier.fillMaxWidth()
-            )
-        },
-        onConfirm = {
-            onAddButtonGroupFromKinds(
-                browsingSourceUrl!!,
-                currentTargetSetId,
-                tempButtonGroupTitle,
-                selectedKindTitles.toList()
-            )
-            selectedKindTitles = emptySet()
-            showAddButtonGroupDialog = false
-        },
-        confirmText = stringResource(R.string.dialog_confirm),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { showAddButtonGroupDialog = false }
-    )
-
-    AppAlertDialog(
-        data = customSetTitleEdit,
-        onDismissRequest = { customSetTitleEdit = null },
-        title = stringResource(R.string.homepage_custom_title),
-        content = { (_, title) ->
-            LaunchedEffect(title) { titleState = title }
-            AppTextField(
-                value = titleState,
-                onValueChange = { titleState = it },
-                label = stringResource(R.string.homepage_title_label),
-                modifier = Modifier.fillMaxWidth()
-            )
-        },
-        onConfirm = { (id, _) ->
-            onSetCustomSetTitle(id, titleState.takeIf { it.isNotBlank() })
-            customSetTitleEdit = null
-        },
-        confirmText = stringResource(R.string.dialog_confirm),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = { customSetTitleEdit = null }
-    )
-}
 
 
-data class AddDialogPrefill(
-    val title: String = "",
-    val url: String = "",
-    val type: String = "card"
-)
-
-@Composable
-fun <T> AddCustomModuleDialog(
-    data: T?,
-    sourceUrl: String = "",
-    targetSetId: String = "",
-    prefillTitle: String = "",
-    prefillUrl: String = "",
-    prefillType: String = "card",
-    prefillArgs: String = "",
-    prefillLayoutConfig: String = "",
-    onDismissRequest: () -> Unit,
-    onConfirm: (ModuleDef) -> Unit,
-) {
-    var title by remember(data) { mutableStateOf(prefillTitle) }
-    var url by remember(data) { mutableStateOf(prefillUrl) }
-    var type by remember(data) { mutableStateOf(prefillType) }
-    var args by remember(data) { mutableStateOf(prefillArgs) }
-    var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) }
-    var showRawLayoutConfig by remember(data) { mutableStateOf(false) }
-
-    val hasVisualizableKeys = remember(layoutConfig) {
-        runCatching {
-            val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject
-            jsonObject.keySet().any { key ->
-                key == "columns" || key == "rows"
-            }
-        }.getOrElse { false }
-    }
-
-    AppAlertDialog(
-        data = data,
-        onDismissRequest = onDismissRequest,
-        title = if (prefillTitle.isEmpty()) stringResource(R.string.homepage_add_module) else stringResource(
-            R.string.homepage_edit_module
-        ),
-        content = {
-            Column(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .height(400.dp)
-                    .verticalScroll(rememberScrollState()),
-                verticalArrangement = Arrangement.spacedBy(8.dp),
-                horizontalAlignment = Alignment.CenterHorizontally
-            ) {
+        var tempName by remember { mutableStateOf("") }
+        AppAlertDialog(
+            data = renameSetId,
+            onDismissRequest = { renameSetId = null },
+            title = stringResource(R.string.homepage_rename_custom_set),
+            content = { setId ->
+                val currentName =
+                    remember(setId) { state.sets.find { it.sourceUrl == setId }?.sourceName ?: "" }
+                LaunchedEffect(setId) { tempName = currentName }
                 AppTextField(
                 AppTextField(
-                    value = title,
-                    onValueChange = { title = it },
-                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
-                    label = stringResource(R.string.homepage_title_label),
+                    value = tempName,
+                    onValueChange = { tempName = it },
+                    label = stringResource(R.string.homepage_name_label),
                     modifier = Modifier.fillMaxWidth()
                     modifier = Modifier.fillMaxWidth()
                 )
                 )
+            },
+            onConfirm = { setId ->
+                if (tempName.isNotBlank()) actions.onRenameCustomSet(
+                    HomepageViewModel.customSetIdFromUrl(setId),
+                    tempName
+                )
+                renameSetId = null
+            },
+            confirmText = stringResource(R.string.dialog_confirm),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { renameSetId = null }
+        )
+
+        AppAlertDialog(
+            data = if (showCreateSetDialog) Unit else null,
+            onDismissRequest = { showCreateSetDialog = false },
+            title = stringResource(R.string.homepage_new_custom_set_title),
+            content = {
+                LaunchedEffect(Unit) { tempName = "" }
                 AppTextField(
                 AppTextField(
-                    value = url,
-                    onValueChange = { url = it },
-                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
-                    label = "URL",
+                    value = tempName,
+                    onValueChange = { tempName = it },
+                    label = stringResource(R.string.homepage_name_label),
                     modifier = Modifier.fillMaxWidth()
                     modifier = Modifier.fillMaxWidth()
                 )
                 )
-                val typeList = remember {
-                    HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
-                }
-
-                GlassCard(
-                    containerColor = LegadoTheme.colorScheme.onSheetContent
-                ) {
-                    DropdownListSettingItem(
-                        title = stringResource(R.string.homepage_type_label),
-                        selectedValue = type,
-                        displayEntries = typeList.map { it.title }.toTypedArray(),
-                        entryValues = typeList.map { it.key }.toTypedArray(),
-                        onValueChange = { type = it }
-                    )
-                }
-
+            },
+            onConfirm = {
+                if (tempName.isNotBlank()) actions.onCreateCustomSet(tempName)
+                showCreateSetDialog = false
+            },
+            confirmText = stringResource(R.string.dialog_confirm),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { showCreateSetDialog = false }
+        )
+
+        AppAlertDialog(
+            data = deleteSetConfirmId,
+            onDismissRequest = { deleteSetConfirmId = null },
+            title = stringResource(R.string.homepage_delete_custom_set),
+            text = stringResource(R.string.homepage_delete_custom_set_confirm),
+            onConfirm = { setId ->
+                actions.onDeleteCustomSet(HomepageViewModel.customSetIdFromUrl(setId))
+                deleteSetConfirmId = null
+            },
+            confirmText = stringResource(R.string.delete),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { deleteSetConfirmId = null }
+        )
+
+        AppAlertDialog(
+            data = deleteConfirmId,
+            onDismissRequest = { deleteConfirmId = null },
+            title = stringResource(R.string.homepage_remove_module),
+            text = stringResource(R.string.homepage_remove_module_confirm),
+            onConfirm = { id ->
+                actions.onDeleteModule(id)
+                deleteConfirmId = null
+            },
+            confirmText = stringResource(R.string.remove),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { deleteConfirmId = null }
+        )
+
+        AddCustomModuleDialog(
+            data = addDialogPrefill,
+            sourceUrl = browsingSourceUrl ?: "",
+            targetSetId = effectiveTargetSetId ?: "",
+            prefillTitle = addDialogPrefill?.title ?: "",
+            prefillUrl = addDialogPrefill?.url ?: "",
+            prefillType = addDialogPrefill?.type ?: "card",
+            canSelectInfinite = canSelectInfiniteGlobal,
+            onDismissRequest = { addDialogPrefill = null },
+            onConfirm = { def ->
+                actions.onAddCustomModule(browsingSourceUrl!!, effectiveTargetSetId, def)
+                addDialogPrefill = null
+            }
+        )
+
+        AddCustomModuleDialog(
+            data = editingModule,
+            sourceUrl = editingModule?.sourceUrl ?: "",
+            targetSetId = editingModule?.customSetId ?: "",
+            prefillTitle = editingModule?.title ?: "",
+            prefillUrl = editingModule?.url ?: "",
+            prefillType = editingModule?.type ?: "card",
+            prefillArgs = editingModule?.args ?: "",
+            prefillLayoutConfig = editingModule?.layoutConfig ?: "",
+            canSelectInfinite = canSelectInfiniteGlobal || (editingModule?.id != null && editingModule?.id == infiniteModuleIdInTargetSet),
+            onDismissRequest = { editingModule = null },
+            onConfirm = { def ->
+                actions.onUpdateModule(editingModule!!.id, def)
+                editingModule = null
+            }
+        )
+
+        var titleState by remember { mutableStateOf("") }
+        AppAlertDialog(
+            data = if (showAddButtonGroupDialog) Unit else null,
+            onDismissRequest = { showAddButtonGroupDialog = false },
+            title = stringResource(R.string.homepage_add_button_group),
+            content = {
+                val quickActionsLabel = stringResource(R.string.homepage_quick_actions)
+                LaunchedEffect(Unit) { tempButtonGroupTitle = quickActionsLabel }
                 AppTextField(
                 AppTextField(
-                    value = args,
-                    onValueChange = { args = it },
-                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
-                    label = "Args (JSON)",
+                    value = tempButtonGroupTitle,
+                    onValueChange = { tempButtonGroupTitle = it },
+                    label = stringResource(R.string.homepage_module_title_label),
                     modifier = Modifier.fillMaxWidth()
                     modifier = Modifier.fillMaxWidth()
                 )
                 )
-
-                PillHeaderDivider(
-                    title = stringResource(R.string.homepage_layout_config_label)
+            },
+            onConfirm = {
+                actions.onAddButtonGroupFromKinds(
+                    browsingSourceUrl!!,
+                    effectiveTargetSetId,
+                    tempButtonGroupTitle,
+                    selectedKindTitles.toList()
                 )
                 )
-
-                if (hasVisualizableKeys) {
-                    JsonConfigEditor(
-                        jsonString = layoutConfig,
-                        onJsonStringChange = { layoutConfig = it },
-                        modifier = Modifier.fillMaxWidth()
-                    )
-                    CompactClickableSettingItem(
-                        title = stringResource(R.string.homepage_edit_raw_json),
-                        onClick = { showRawLayoutConfig = !showRawLayoutConfig }
-                    )
-                    if (showRawLayoutConfig) {
-                        JsonRawEditor(
-                            value = layoutConfig,
-                            onValueChange = { layoutConfig = it },
-                            label = "LayoutConfig (JSON) RAW",
-                            modifier = Modifier.fillMaxWidth()
-                        )
-                    }
-                } else {
-                    JsonRawEditor(
-                        value = layoutConfig,
-                        onValueChange = { layoutConfig = it },
-                        label = "LayoutConfig (JSON)",
-                        modifier = Modifier.fillMaxWidth()
-                    )
-                }
-            }
-        },
-        onConfirm = {
-            onConfirm(
-                ModuleDef(
-                    title = title,
-                    url = url,
-                    type = type,
-                    args = args,
-                    layoutConfig = layoutConfig
+                selectedKindTitles = emptySet()
+                showAddButtonGroupDialog = false
+            },
+            confirmText = stringResource(R.string.dialog_confirm),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { showAddButtonGroupDialog = false }
+        )
+
+        AppAlertDialog(
+            data = customSetTitleEdit,
+            onDismissRequest = { customSetTitleEdit = null },
+            title = stringResource(R.string.homepage_custom_title),
+            content = { (_, title) ->
+                LaunchedEffect(title) { titleState = title }
+                AppTextField(
+                    value = titleState,
+                    onValueChange = { titleState = it },
+                    label = stringResource(R.string.homepage_title_label),
+                    modifier = Modifier.fillMaxWidth()
                 )
                 )
-            )
-        },
-        confirmText = stringResource(R.string.dialog_confirm),
-        dismissText = stringResource(R.string.dialog_cancel),
-        onDismiss = onDismissRequest
-    )
+            },
+            onConfirm = { (id, _) ->
+                actions.onSetCustomSetTitle(id, titleState.takeIf { it.isNotBlank() })
+                customSetTitleEdit = null
+            },
+            confirmText = stringResource(R.string.dialog_confirm),
+            dismissText = stringResource(R.string.dialog_cancel),
+            onDismiss = { customSetTitleEdit = null }
+        )
+    }
 }
 }

+ 42 - 47
app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt

@@ -93,15 +93,16 @@ fun HomepageScreen(
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
 ) {
 ) {
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-    val allSets by viewModel.setsFlow.collectAsStateWithLifecycle()
-    val browseSources by viewModel.browseSourcesFlow.collectAsStateWithLifecycle()
+    // Removed allSets and browseSources as they are now part of uiState.manageState
     val scope = rememberCoroutineScope()
     val scope = rememberCoroutineScope()
     val context = LocalContext.current
     val context = LocalContext.current
     var errorMsg by remember { mutableStateOf<String?>(null) }
     var errorMsg by remember { mutableStateOf<String?>(null) }
 
 
     val layoutMode = HomepageConfig.homepageLayoutModeState.value
     val layoutMode = HomepageConfig.homepageLayoutModeState.value
 
 
-    val selectedSets = remember(allSets) { allSets.filter { it.isSelected } }
+    val selectedSets = remember(uiState.manageState.sets) {
+        uiState.manageState.sets.filter { it.isSelected }
+    }
     val pagerState = rememberPagerState(pageCount = {
     val pagerState = rememberPagerState(pageCount = {
         if (layoutMode == 1) selectedSets.size.coerceAtLeast(1) else 1
         if (layoutMode == 1) selectedSets.size.coerceAtLeast(1) else 1
     })
     })
@@ -261,50 +262,44 @@ fun HomepageScreen(
         HomepageModuleManageSheet(
         HomepageModuleManageSheet(
             data = if (uiState.isManageMode) Unit else null,
             data = if (uiState.isManageMode) Unit else null,
             onDismissRequest = { viewModel.toggleManageMode() },
             onDismissRequest = { viewModel.toggleManageMode() },
-            sets = allSets,
-            browseSources = browseSources,
-            onToggleSet = { url, isEnabled -> viewModel.toggleSourceFilter(url, isEnabled) },
-            onGetModulesInSet = { viewModel.getJoinedModules(it) },
-            onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
-            onSyncSourceModules = { viewModel.syncSourceModules(it) },
-            onToggleModule = { id, visible -> viewModel.setModuleVisible(id, visible) },
-            onJoinModule = { sourceUrl, targetSetId, def ->
-                viewModel.joinModule(
-                    sourceUrl,
-                    targetSetId,
-                    def
-                )
-            },
-            onAddCustomModule = { sourceUrl, targetSetId, def ->
-                viewModel.addCustomModule(
-                    sourceUrl,
-                    targetSetId,
-                    def
-                )
-            },
-            onAddButtonGroupFromKinds = { sourceUrl, targetSetId, title, kinds ->
-                viewModel.addButtonGroupFromKinds(
-                    sourceUrl,
-                    targetSetId,
-                    title,
-                    kinds
-                )
-            },
-            onGetExploreKinds = { viewModel.getSourceExploreKinds(it) },
-            onUpdateModule = { globalId, def -> viewModel.updateModule(globalId, def) },
-            onDeleteModule = { viewModel.deleteModule(it) },
-            onReorderModules = { ids -> viewModel.reorderJoinedModules(ids) },
-            onReorderSets = { urls -> viewModel.reorderCustomSets(urls) },
-            onSetCustomSetTitle = { id, title -> viewModel.setModuleCustomSetTitle(id, title) },
-            onCreateCustomSet = { viewModel.createCustomSet(it) },
-            onRenameCustomSet = { id, name -> viewModel.renameCustomSet(id, name) },
-            onDeleteCustomSet = { viewModel.deleteCustomSet(it) },
-            onGetAllModulesGroupedBySource = { viewModel.getAllModulesGroupedBySource() },
-            onGetSourceName = { viewModel.getSourceName(it) },
-            onAssignModuleToCustomSet = { id, setId ->
-                viewModel.assignModuleToCustomSet(
-                    id,
-                    setId
+            state = uiState.manageState,
+            actions = remember {
+                HomepageManageActions(
+                    onToggleSet = { url, isEnabled ->
+                        viewModel.toggleSourceFilter(
+                            url,
+                            isEnabled
+                        )
+                    },
+                    onGetSourceModules = { url, setId -> viewModel.getSourceModules(url, setId) },
+                    onSyncSourceModules = { viewModel.syncSourceModules(it) },
+                    onToggleModule = { id, visible -> viewModel.setModuleVisible(id, visible) },
+                    onJoinModule = { sourceUrl, targetSetId, def ->
+                        viewModel.joinModule(sourceUrl, targetSetId, def)
+                    },
+                    onAddCustomModule = { sourceUrl, targetSetId, def ->
+                        viewModel.addCustomModule(sourceUrl, targetSetId, def)
+                    },
+                    onAddButtonGroupFromKinds = { sourceUrl, targetSetId, title, kinds ->
+                        viewModel.addButtonGroupFromKinds(sourceUrl, targetSetId, title, kinds)
+                    },
+                    onGetExploreKinds = { viewModel.getSourceExploreKinds(it) },
+                    onUpdateModule = { globalId, def -> viewModel.updateModule(globalId, def) },
+                    onDeleteModule = { viewModel.deleteModule(it) },
+                    onReorderModules = { ids -> viewModel.reorderJoinedModules(ids) },
+                    onReorderSets = { urls -> viewModel.reorderCustomSets(urls) },
+                    onSetCustomSetTitle = { id, title ->
+                        viewModel.setModuleCustomSetTitle(
+                            id,
+                            title
+                        )
+                    },
+                    onCreateCustomSet = { viewModel.createCustomSet(it) },
+                    onRenameCustomSet = { id, name -> viewModel.renameCustomSet(id, name) },
+                    onDeleteCustomSet = { viewModel.deleteCustomSet(it) },
+                    onAssignModuleToCustomSet = { id, setId ->
+                        viewModel.assignModuleToCustomSet(id, setId)
+                    }
                 )
                 )
             }
             }
         )
         )

+ 240 - 317
app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt

@@ -82,46 +82,113 @@ class HomepageViewModel(
     val effects = _effects.asSharedFlow()
     val effects = _effects.asSharedFlow()
 
 
     private val loadJobs = ConcurrentHashMap<String, Job>()
     private val loadJobs = ConcurrentHashMap<String, Job>()
-    private val initModulesSyncFlow = bookSourceRepository.flowHomepageModules()
     private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
     private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
 
 
+    // 1. 基础原始状态
     private val _isRefreshing = MutableStateFlow(false)
     private val _isRefreshing = MutableStateFlow(false)
     private val _isManageMode = MutableStateFlow(false)
     private val _isManageMode = MutableStateFlow(false)
     private val _isConfigMode = MutableStateFlow(false)
     private val _isConfigMode = MutableStateFlow(false)
     private val _configVersion = MutableStateFlow(0L)
     private val _configVersion = MutableStateFlow(0L)
     private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
     private val _moduleContentStates = MutableStateFlow<Map<String, ModuleLoadState>>(emptyMap())
-
-    private val localModulesFlow = gateway.flowEnabled()
     private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
     private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
     private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(emptyMap())
     private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(emptyMap())
+    private val _pendingEnabled = MutableStateFlow<Map<String, Boolean>>(emptyMap())
+    private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList())
+    private val _exploreKindsCache =
+        MutableStateFlow<Map<String, List<Pair<String, String>>>>(emptyMap())
 
 
-    val allModulesCache = gateway.flowAll()
-        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
-
+    // 2. 数据库同步流
+    private val localModulesFlow = gateway.flowEnabled()
+    val allModulesCache =
+        gateway.flowAll().stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
     val customSetsFlow = gateway.flowCustomSets()
     val customSetsFlow = gateway.flowCustomSets()
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
         .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
 
 
+    // 3. 业务加工流
     private val orderedModuleDefsFlow = combine(localModulesFlow, _configVersion) { modules, _ ->
     private val orderedModuleDefsFlow = combine(localModulesFlow, _configVersion) { modules, _ ->
         modules.groupBySourceOrdered()
         modules.groupBySourceOrdered()
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
 
 
+    val setsFlow = combine(
+        localModulesFlow,
+        allModulesCache,
+        customSetsFlow,
+        _configVersion
+    ) { _, allModules, customSets, _ ->
+        val hiddenSourceUrls = GSON.fromJsonArray<String>(HomepageConfig.homepageSourceHidden)
+            .getOrDefault(emptyList()).toSet()
+        val moduleCountsBySet =
+            allModules.mapNotNull { it.customSetId }.groupBy { it }.mapValues { it.value.size }
+
+        customSets.sortedBy { it.sortOrder }.map { set ->
+            HomepageSourceManageUi(
+                sourceUrl = customSetUrl(set.id),
+                sourceName = set.name,
+                sourceGroup = null,
+                isSelected = customSetUrl(set.id) !in hiddenSourceUrls,
+                moduleCount = moduleCountsBySet[set.id] ?: 0,
+                isCustomSet = true,
+            )
+        }.toImmutableList()
+    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
+
+    val browseSourcesFlow = exploreSourcesFlow.map { sources ->
+        sources.map { source ->
+            HomepageSourceManageUi(
+                sourceUrl = source.bookSourceUrl,
+                sourceName = source.bookSourceName,
+                sourceGroup = source.bookSourceGroup,
+            )
+        }.toImmutableList()
+    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
+
+    // 4. 聚合层
     private val uiFlagsFlow =
     private val uiFlagsFlow =
         combine(_isRefreshing, _isManageMode, _isConfigMode) { refreshing, manage, config ->
         combine(_isRefreshing, _isManageMode, _isConfigMode) { refreshing, manage, config ->
             HomepageUiFlags(refreshing, manage, config)
             HomepageUiFlags(refreshing, manage, config)
         }
         }
 
 
-    val uiState: StateFlow<HomepageUiState> = combine(
+    private val manageStateFlow = combine(
+        setsFlow,
+        browseSourcesFlow,
+        allModulesCache,
+        _bookSourcesCache,
+        _pendingEnabled
+    ) { sets, browseSources, allModules, sourcesCache, pendingEnabled ->
+        HomepageManageUiState(
+            sets = sets,
+            browseSources = browseSources,
+            allJoinedModules = allModules.map { module ->
+                HomepageModuleManageUi(
+                    id = module.id,
+                    sourceUrl = module.sourceUrl,
+                    moduleKey = module.moduleKey,
+                    title = module.displayTitle,
+                    customSetTitle = module.customSetTitle,
+                    customSetId = module.customSetId,
+                    isVisible = pendingEnabled[module.id] ?: module.isEnabled,
+                    type = module.type,
+                    url = module.url,
+                    args = module.args,
+                    layoutConfig = module.layoutConfig,
+                    originalTitle = module.title,
+                )
+            }.toImmutableList(),
+            sourceNames = sourcesCache.mapValues { it.value.bookSourceName }
+        )
+    }
+
+    private val displayModulesFlow = combine(
         orderedModuleDefsFlow,
         orderedModuleDefsFlow,
         _moduleContentStates,
         _moduleContentStates,
-        uiFlagsFlow,
         _bookSourcesCache,
         _bookSourcesCache,
-        customSetsFlow
-    ) { grouped, contentStates, flags, sourcesCache, customSets ->
+        customSetsFlow,
+        _layoutConfigCache
+    ) { grouped, contentStates, sourcesCache, customSets, configCache ->
         val setNames = customSets.associate { it.id to it.name }
         val setNames = customSets.associate { it.id to it.name }
         val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id }
         val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id }
-        val configCache = _layoutConfigCache.value
 
 
-        val displayModules = sortedSetIds.flatMap { setId ->
+        sortedSetIds.flatMap { setId ->
             val setUrl = customSetUrl(setId)
             val setUrl = customSetUrl(setId)
             val mods = grouped[setUrl] ?: emptyList()
             val mods = grouped[setUrl] ?: emptyList()
             mods.map { module ->
             mods.map { module ->
@@ -144,60 +211,26 @@ class HomepageViewModel(
                     config = configMap
                     config = configMap
                 )
                 )
             }
             }
-        }
+        }.toImmutableList()
+    }
+
+    // 5. 最终 UI 状态
+    val uiState: StateFlow<HomepageUiState> = combine(
+        displayModulesFlow,
+        uiFlagsFlow,
+        manageStateFlow
+    ) { modules, flags, manageState ->
         HomepageUiState(
         HomepageUiState(
-            modules = displayModules.toImmutableList(),
+            modules = modules,
             isRefreshing = flags.isRefreshing,
             isRefreshing = flags.isRefreshing,
             isManageMode = flags.isManageMode,
             isManageMode = flags.isManageMode,
             isConfigMode = flags.isConfigMode,
             isConfigMode = flags.isConfigMode,
+            manageState = manageState
         )
         )
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), HomepageUiState())
 
 
-    val setsFlow = combine(
-        localModulesFlow,
-        allModulesCache,
-        customSetsFlow,
-        _configVersion
-    ) { _, allModules, customSets, _ ->
-        val hiddenSourceUrls = GSON.fromJsonArray<String>(HomepageConfig.homepageSourceHidden)
-            .getOrDefault(emptyList()).toSet()
-        val moduleCountsBySet =
-            allModules.mapNotNull { it.customSetId }.groupBy { it }.mapValues { it.value.size }
-
-        val list = mutableListOf<HomepageSourceManageUi>()
-        customSets.sortedBy { it.sortOrder }.forEach { set ->
-            list.add(
-                HomepageSourceManageUi(
-                    sourceUrl = customSetUrl(set.id),
-                    sourceName = set.name,
-                    sourceGroup = null,
-                    isSelected = customSetUrl(set.id) !in hiddenSourceUrls,
-                    moduleCount = moduleCountsBySet[set.id] ?: 0,
-                    isCustomSet = true,
-                )
-            )
-        }
-        list.toImmutableList()
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
-
-    /** 用于「浏览书源模块」:列出有 homepageModules 的书源 */
-    val browseSourcesFlow = exploreSourcesFlow.map { sources ->
-        sources.map { source ->
-            HomepageSourceManageUi(
-                sourceUrl = source.bookSourceUrl,
-                sourceName = source.bookSourceName,
-                sourceGroup = source.bookSourceGroup,
-            )
-        }.toImmutableList()
-    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())
-
-    private val _exploreKindsCache =
-        MutableStateFlow<Map<String, List<Pair<String, String>>>>(emptyMap())
-    private val _pendingEnabled = MutableStateFlow<Map<String, Boolean>>(emptyMap())
-    private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList())
-
     init {
     init {
-        // 解析并缓存模块 layoutConfig,避免在 combine 中重复解析
+        // 解析并缓存模块 layoutConfig
         viewModelScope.launch {
         viewModelScope.launch {
             localModulesFlow.collect { modules ->
             localModulesFlow.collect { modules ->
                 val cache = mutableMapOf<String, Map<String, String>>()
                 val cache = mutableMapOf<String, Map<String, String>>()
@@ -217,7 +250,6 @@ class HomepageViewModel(
             }
             }
         }
         }
 
 
-        // 清理 _pendingUserModules 中已入库的条目
         viewModelScope.launch {
         viewModelScope.launch {
             exploreSourcesFlow.collect { sources ->
             exploreSourcesFlow.collect { sources ->
                 _bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
                 _bookSourcesCache.value = sources.associateBy { it.bookSourceUrl }
@@ -235,7 +267,6 @@ class HomepageViewModel(
             }
             }
         }
         }
 
 
-        // 清理 _pendingUserModules 中已入库的条目
         viewModelScope.launch {
         viewModelScope.launch {
             allModulesCache.collect { modules ->
             allModulesCache.collect { modules ->
                 val dbIds = modules.map { it.id }.toSet()
                 val dbIds = modules.map { it.id }.toSet()
@@ -243,7 +274,6 @@ class HomepageViewModel(
             }
             }
         }
         }
 
 
-        // 一次性迁移:将 customSetId=null 的存量模块归属到书源集,并确保所有集存在
         viewModelScope.launch {
         viewModelScope.launch {
             val allModules = allModulesCache.first()
             val allModules = allModulesCache.first()
             val orphans = allModules.filter { it.customSetId == null }
             val orphans = allModules.filter { it.customSetId == null }
@@ -251,20 +281,15 @@ class HomepageViewModel(
                 orphans.groupBy { it.sourceUrl }.forEach { (sourceUrl, modules) ->
                 orphans.groupBy { it.sourceUrl }.forEach { (sourceUrl, modules) ->
                     val source = bookSourceRepository.getBookSource(sourceUrl) ?: return@forEach
                     val source = bookSourceRepository.getBookSource(sourceUrl) ?: return@forEach
                     ensureSetForSource(sourceUrl, source.bookSourceName)
                     ensureSetForSource(sourceUrl, source.bookSourceName)
-                    modules.forEach { m ->
-                        gateway.setCustomSetId(m.id, "src_$sourceUrl")
-                    }
+                    modules.forEach { m -> gateway.setCustomSetId(m.id, "src_$sourceUrl") }
                 }
                 }
             }
             }
-            // 确保所有 customSetId 对应的集都存在
             allModules.mapNotNull { it.customSetId }.distinct().forEach { setId ->
             allModules.mapNotNull { it.customSetId }.distinct().forEach { setId ->
                 val isSrcSet = setId.startsWith("src_")
                 val isSrcSet = setId.startsWith("src_")
                 if (isSrcSet && gateway.getCustomSetById(setId) == null) {
                 if (isSrcSet && gateway.getCustomSetById(setId) == null) {
                     val sourceUrl = setId.removePrefix("src_")
                     val sourceUrl = setId.removePrefix("src_")
                     val source = bookSourceRepository.getBookSource(sourceUrl)
                     val source = bookSourceRepository.getBookSource(sourceUrl)
-                    if (source != null) {
-                        ensureSetForSource(sourceUrl, source.bookSourceName)
-                    }
+                    if (source != null) ensureSetForSource(sourceUrl, source.bookSourceName)
                 }
                 }
             }
             }
         }
         }
@@ -281,22 +306,16 @@ class HomepageViewModel(
         val parsedIds = parsedDefs.map { it.globalId }.toSet()
         val parsedIds = parsedDefs.map { it.globalId }.toSet()
 
 
         val toUpsert = mutableListOf<ModuleItem>()
         val toUpsert = mutableListOf<ModuleItem>()
-
         for (i in parsedDefs.indices) {
         for (i in parsedDefs.indices) {
             val def = parsedDefs[i]
             val def = parsedDefs[i]
             val existing = existingById[def.globalId]
             val existing = existingById[def.globalId]
             if (existing != null) {
             if (existing != null) {
-                // 用户编辑过的模块不被 JSON 覆盖
                 if (existing.isUserCreated) continue
                 if (existing.isUserCreated) continue
                 if (existing.sourceJsonHash == newHash) continue
                 if (existing.sourceJsonHash == newHash) continue
                 toUpsert.add(
                 toUpsert.add(
                     existing.copy(
                     existing.copy(
-                        type = def.type,
-                        title = def.title,
-                        args = def.args,
-                        url = def.url,
-                        sourceJsonHash = newHash,
-                        syncedAt = System.currentTimeMillis(),
+                        type = def.type, title = def.title, args = def.args, url = def.url,
+                        sourceJsonHash = newHash, syncedAt = System.currentTimeMillis()
                     )
                     )
                 )
                 )
             } else {
             } else {
@@ -313,19 +332,13 @@ class HomepageViewModel(
                         customSetId = "src_${source.bookSourceUrl}",
                         customSetId = "src_${source.bookSourceUrl}",
                         sortOrder = i,
                         sortOrder = i,
                         sourceJsonHash = newHash,
                         sourceJsonHash = newHash,
-                        syncedAt = System.currentTimeMillis(),
+                        syncedAt = System.currentTimeMillis()
                     )
                     )
                 )
                 )
             }
             }
         }
         }
-
-        if (toUpsert.isNotEmpty()) {
-            gateway.upsertAll(toUpsert)
-        }
-
-        if (parsedIds.isNotEmpty()) {
-            gateway.deleteStale(source.bookSourceUrl, parsedIds.toList())
-        }
+        if (toUpsert.isNotEmpty()) gateway.upsertAll(toUpsert)
+        if (parsedIds.isNotEmpty()) gateway.deleteStale(source.bookSourceUrl, parsedIds.toList())
     }
     }
 
 
     private fun loadModule(module: ModuleItem) {
     private fun loadModule(module: ModuleItem) {
@@ -336,74 +349,50 @@ class HomepageViewModel(
                     val source = bookSourceRepository.getBookSource(module.sourceUrl)
                     val source = bookSourceRepository.getBookSource(module.sourceUrl)
                         ?: throw Exception("Source not found")
                         ?: throw Exception("Source not found")
                     val allKinds = withContext(Dispatchers.IO) { source.exploreKinds() }
                     val allKinds = withContext(Dispatchers.IO) { source.exploreKinds() }
-
-                    val selectedTitles = module.args?.let { argsStr ->
-                        GSON.fromJsonArray<String>(argsStr).getOrNull()
-                    }
-
-                    if (selectedTitles.isNullOrEmpty()) {
-                        allKinds.take(HOMEPAGE_MAX_BUTTON_GROUP_KINDS)
-                    } else {
-                        selectedTitles.mapNotNull { t -> allKinds.find { it.title == t } }
-                    }
+                    val selectedTitles =
+                        module.args?.let { GSON.fromJsonArray<String>(it).getOrNull() }
+                    if (selectedTitles.isNullOrEmpty()) allKinds.take(
+                        HOMEPAGE_MAX_BUTTON_GROUP_KINDS
+                    )
+                    else selectedTitles.mapNotNull { t -> allKinds.find { it.title == t } }
                 }.onSuccess { kinds ->
                 }.onSuccess { kinds ->
                     _moduleContentStates.update { it + (module.id to ModuleLoadState.Buttons(kinds.toImmutableList())) }
                     _moduleContentStates.update { it + (module.id to ModuleLoadState.Buttons(kinds.toImmutableList())) }
                 }.onFailure { e ->
                 }.onFailure { e ->
-                    _moduleContentStates.update {
-                        it + (module.id to ModuleLoadState.Error(
-                            e.stackTraceStr
-                        ))
-                    }
+                    _moduleContentStates.update { it + (module.id to ModuleLoadState.Error(e.stackTraceStr)) }
                 }
                 }
-            }.also {
-                it.invokeOnCompletion { loadJobs.remove(module.id) }
-            }
+            }.also { it.invokeOnCompletion { loadJobs.remove(module.id) } }
             return
             return
         }
         }
         loadJobs[module.id] = viewModelScope.launch {
         loadJobs[module.id] = viewModelScope.launch {
             kotlin.runCatching {
             kotlin.runCatching {
-                val isRanking = module.type == HomepageModuleType.Ranking.key
-                        || module.type == HomepageModuleType.GridRanking.key
-                val books = if (isRanking) {
-                    exploreBooksUseCase.executeForRanking(module.sourceUrl, module.url, module.args)
-                } else {
-                    exploreBooksUseCase.execute(module.sourceUrl, module.url, module.args).books
-                }
-                val layout = try {
-                    GSON.fromJson(module.layoutConfig, Map::class.java)
-                } catch (_: Exception) {
-                    null
-                }
-                val rows = (layout?.get("rows") as? Number)?.toInt() ?: HOMEPAGE_DEFAULT_GRID_ROWS
+                val isRanking =
+                    module.type == HomepageModuleType.Ranking.key || module.type == HomepageModuleType.GridRanking.key
+                val books = if (isRanking) exploreBooksUseCase.executeForRanking(
+                    module.sourceUrl,
+                    module.url,
+                    module.args
+                )
+                else exploreBooksUseCase.execute(module.sourceUrl, module.url, module.args).books
                 val hasMore = isInfinite(module.type, module.layoutConfig) && books.isNotEmpty()
                 val hasMore = isInfinite(module.type, module.layoutConfig) && books.isNotEmpty()
                 books to hasMore
                 books to hasMore
             }.onSuccess { (books, hasMore) ->
             }.onSuccess { (books, hasMore) ->
                 _moduleContentStates.update {
                 _moduleContentStates.update {
                     it + (module.id to ModuleLoadState.Loaded(
                     it + (module.id to ModuleLoadState.Loaded(
                         books.toImmutableList(),
                         books.toImmutableList(),
-                        hasMore = hasMore,
-                        page = 1
+                        hasMore = hasMore
                     ))
                     ))
                 }
                 }
             }.onFailure { e ->
             }.onFailure { e ->
-                _moduleContentStates.update {
-                    it + (module.id to ModuleLoadState.Error(
-                        e.stackTraceStr
-                    ))
-                }
+                _moduleContentStates.update { it + (module.id to ModuleLoadState.Error(e.stackTraceStr)) }
             }
             }
-        }.also {
-            it.invokeOnCompletion { loadJobs.remove(module.id) }
-        }
+        }.also { it.invokeOnCompletion { loadJobs.remove(module.id) } }
     }
     }
 
 
     fun loadMoreModule(globalId: String) {
     fun loadMoreModule(globalId: String) {
         val currentState = _moduleContentStates.value[globalId] as? ModuleLoadState.Loaded ?: return
         val currentState = _moduleContentStates.value[globalId] as? ModuleLoadState.Loaded ?: return
         if (currentState.isLoadingMore || !currentState.hasMore) return
         if (currentState.isLoadingMore || !currentState.hasMore) return
-
         val nextPage = currentState.page + 1
         val nextPage = currentState.page + 1
         _moduleContentStates.update { it + (globalId to currentState.copy(isLoadingMore = true)) }
         _moduleContentStates.update { it + (globalId to currentState.copy(isLoadingMore = true)) }
-
         viewModelScope.launch {
         viewModelScope.launch {
             kotlin.runCatching {
             kotlin.runCatching {
                 val module = gateway.getById(globalId) ?: throw Exception("Module not found")
                 val module = gateway.getById(globalId) ?: throw Exception("Module not found")
@@ -414,18 +403,14 @@ class HomepageViewModel(
                     page = nextPage
                     page = nextPage
                 )
                 )
             }.onSuccess { result ->
             }.onSuccess { result ->
-                val newBooks = result.books
                 _moduleContentStates.update { states ->
                 _moduleContentStates.update { states ->
                     val lastState =
                     val lastState =
                         states[globalId] as? ModuleLoadState.Loaded ?: return@update states
                         states[globalId] as? ModuleLoadState.Loaded ?: return@update states
                     val existingUrls = lastState.books.map { it.bookUrl }.toSet()
                     val existingUrls = lastState.books.map { it.bookUrl }.toSet()
-                    val deduped = newBooks.filter { it.bookUrl !in existingUrls }
-                    val combinedBooks = (lastState.books + deduped).toImmutableList()
+                    val deduped = result.books.filter { it.bookUrl !in existingUrls }
                     states + (globalId to ModuleLoadState.Loaded(
                     states + (globalId to ModuleLoadState.Loaded(
-                        books = combinedBooks,
-                        hasMore = deduped.isNotEmpty(),
-                        isLoadingMore = false,
-                        page = nextPage
+                        books = (lastState.books + deduped).toImmutableList(),
+                        hasMore = deduped.isNotEmpty(), isLoadingMore = false, page = nextPage
                     ))
                     ))
                 }
                 }
             }.onFailure { e ->
             }.onFailure { e ->
@@ -461,17 +446,12 @@ class HomepageViewModel(
             _isRefreshing.value = true
             _isRefreshing.value = true
             loadJobs.values.forEach { it.cancel() }
             loadJobs.values.forEach { it.cancel() }
             loadJobs.clear()
             loadJobs.clear()
-
-            // 刷新时同步当前已启用模块所属书源的定义
-            val activeSourceUrls = uiState.value.modules.map { it.sourceUrl }.distinct()
-            activeSourceUrls.forEach { url ->
+            uiState.value.modules.map { it.sourceUrl }.distinct().forEach { url ->
                 resolveBookSource(url)?.let { syncModulesFromSource(it) }
                 resolveBookSource(url)?.let { syncModulesFromSource(it) }
             }
             }
-
             _moduleContentStates.value = emptyMap()
             _moduleContentStates.value = emptyMap()
-            uiState.map { it.modules }.first { modules ->
-                modules.all { it.state !is ModuleLoadState.Loading }
-            }
+            uiState.map { it.modules }
+                .first { modules -> modules.all { it.state !is ModuleLoadState.Loading } }
             _isRefreshing.value = false
             _isRefreshing.value = false
         }
         }
     }
     }
@@ -486,14 +466,11 @@ class HomepageViewModel(
     fun setModuleVisible(id: String, visible: Boolean) {
     fun setModuleVisible(id: String, visible: Boolean) {
         _pendingEnabled.update { it + (id to visible) }
         _pendingEnabled.update { it + (id to visible) }
         viewModelScope.launch {
         viewModelScope.launch {
-            val existing = gateway.getById(id)
-            if (existing != null) {
-                gateway.setEnabled(id, visible)
-            } else {
-                // 如果模块尚未入库(虚拟状态),则根据 ID 规则解析并入库
+            if (gateway.getById(id) != null) gateway.setEnabled(id, visible)
+            else {
                 val parts = id.split("::")
                 val parts = id.split("::")
                 if (parts.size >= 3) {
                 if (parts.size >= 3) {
-                    val setId = parts[0]
+                    val setId = parts[0];
                     val sourceUrl = parts[1]
                     val sourceUrl = parts[1]
                     val key = parts.subList(2, parts.size).joinToString("::")
                     val key = parts.subList(2, parts.size).joinToString("::")
                     ensureModuleInDb(sourceUrl, key, id, setId)
                     ensureModuleInDb(sourceUrl, key, id, setId)
@@ -520,24 +497,25 @@ class HomepageViewModel(
 
 
     private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
     private suspend fun ensureSetForSource(sourceUrl: String, sourceName: String): String {
         val setId = "src_$sourceUrl"
         val setId = "src_$sourceUrl"
-        if (gateway.getCustomSetById(setId) == null) {
-            gateway.upsertCustomSet(CustomSetItem(id = setId, name = sourceName))
-        }
+        if (gateway.getCustomSetById(setId) == null) gateway.upsertCustomSet(
+            CustomSetItem(
+                id = setId,
+                name = sourceName
+            )
+        )
         return setId
         return setId
     }
     }
 
 
     fun addCustomModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
     fun addCustomModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
-        val key = def.key.ifBlank { def.title }
+        val key = def.key.ifBlank { def.title };
         val setId = targetSetId ?: "src_$sourceUrl"
         val setId = targetSetId ?: "src_$sourceUrl"
-
         if (isInfinite(def.type, def.layoutConfig)) {
         if (isInfinite(def.type, def.layoutConfig)) {
-            val hasInfinite = allModulesCache.value.any {
-                it.customSetId == setId && isInfinite(
-                    it.type,
-                    it.layoutConfig
-                )
-            }
-            if (hasInfinite) {
+            if (allModulesCache.value.any {
+                    it.customSetId == setId && isInfinite(
+                        it.type,
+                        it.layoutConfig
+                    )
+                }) {
                 viewModelScope.launch {
                 viewModelScope.launch {
                     _effects.emit(
                     _effects.emit(
                         HomepageEffect.ShowSnackbar(
                         HomepageEffect.ShowSnackbar(
@@ -550,21 +528,11 @@ class HomepageViewModel(
                 return
                 return
             }
             }
         }
         }
-
         val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
         val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
         val module = ModuleItem(
         val module = ModuleItem(
-            id = id,
-            sourceUrl = sourceUrl,
-            moduleKey = key,
-            type = def.type,
-            title = def.title,
-            args = def.args,
-            layoutConfig = def.layoutConfig,
-            url = def.url,
-            isEnabled = true,
-            isUserCreated = true,
-            customSetId = setId,
-            syncedAt = System.currentTimeMillis(),
+            id = id, sourceUrl = sourceUrl, moduleKey = key, type = def.type, title = def.title,
+            args = def.args, layoutConfig = def.layoutConfig, url = def.url, isEnabled = true,
+            isUserCreated = true, customSetId = setId, syncedAt = System.currentTimeMillis()
         )
         )
         viewModelScope.launch {
         viewModelScope.launch {
             val source = bookSourceRepository.getBookSource(sourceUrl)
             val source = bookSourceRepository.getBookSource(sourceUrl)
@@ -575,23 +543,37 @@ class HomepageViewModel(
         }
         }
     }
     }
 
 
-    fun getSourceExploreKinds(sourceUrl: String): List<Pair<String, String>> {
-        return _exploreKindsCache.value[sourceUrl].orEmpty()
-    }
+    fun getSourceExploreKinds(sourceUrl: String): List<Pair<String, String>> =
+        _exploreKindsCache.value[sourceUrl].orEmpty()
 
 
     fun updateModule(globalId: String, def: ModuleDef) {
     fun updateModule(globalId: String, def: ModuleDef) {
         viewModelScope.launch {
         viewModelScope.launch {
             val existing = gateway.getById(globalId) ?: return@launch
             val existing = gateway.getById(globalId) ?: return@launch
+            // 校验:如果是变更为无限流,检查集内是否已有其他无限流模块
+            if (isInfinite(def.type, def.layoutConfig)) {
+                val hasOtherInfinite = allModulesCache.value.any {
+                    it.customSetId == existing.customSetId && it.id != globalId && isInfinite(
+                        it.type,
+                        it.layoutConfig
+                    )
+                }
+                if (hasOtherInfinite) {
+                    _effects.emit(
+                        HomepageEffect.ShowSnackbar(
+                            getApplication<Application>().getString(
+                                R.string.homepage_module_duplicate_infinite
+                            )
+                        )
+                    )
+                    return@launch
+                }
+            }
             gateway.upsertAll(
             gateway.upsertAll(
                 listOf(
                 listOf(
                     existing.copy(
                     existing.copy(
-                        customTitle = def.title.takeIf { it != existing.title },
-                        type = def.type,
-                        url = def.url,
-                        args = def.args,
-                        layoutConfig = def.layoutConfig,
-                        isUserCreated = true,  // 标记为用户编辑,阻止 JSON 同步覆盖
-                        syncedAt = System.currentTimeMillis(),
+                        customTitle = def.title.takeIf { it != existing.title }, type = def.type,
+                        url = def.url, args = def.args, layoutConfig = def.layoutConfig,
+                        isUserCreated = true, syncedAt = System.currentTimeMillis()
                     )
                     )
                 )
                 )
             )
             )
@@ -601,17 +583,17 @@ class HomepageViewModel(
 
 
     fun setModuleCustomSetTitle(globalId: String, customSetTitle: String?) {
     fun setModuleCustomSetTitle(globalId: String, customSetTitle: String?) {
         viewModelScope.launch {
         viewModelScope.launch {
-            gateway.setCustomSetTitle(globalId, customSetTitle)
-            notifyConfigChanged()
+            gateway.setCustomSetTitle(
+                globalId,
+                customSetTitle
+            ); notifyConfigChanged()
         }
         }
     }
     }
 
 
     fun deleteModule(globalId: String) {
     fun deleteModule(globalId: String) {
         viewModelScope.launch {
         viewModelScope.launch {
-            gateway.delete(globalId)
-            _moduleContentStates.update { it - globalId }
-            loadJobs.remove(globalId)?.cancel()
-            _pendingEnabled.update { it - globalId }
+            gateway.delete(globalId); _moduleContentStates.update { it - globalId }
+            loadJobs.remove(globalId)?.cancel(); _pendingEnabled.update { it - globalId }
             _pendingUserModules.update { it.filter { m -> m.id != globalId } }
             _pendingUserModules.update { it.filter { m -> m.id != globalId } }
             notifyConfigChanged()
             notifyConfigChanged()
         }
         }
@@ -620,30 +602,23 @@ class HomepageViewModel(
     fun reorderJoinedModules(orderedIds: List<String>) {
     fun reorderJoinedModules(orderedIds: List<String>) {
         viewModelScope.launch {
         viewModelScope.launch {
             val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap()
             val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap()
-            gateway.batchSetSortOrders(orders)
-            notifyConfigChanged()
+            gateway.batchSetSortOrders(orders); notifyConfigChanged()
         }
         }
     }
     }
 
 
     fun reorderCustomSets(orderedUrls: List<String>) {
     fun reorderCustomSets(orderedUrls: List<String>) {
         viewModelScope.launch {
         viewModelScope.launch {
-            val orders = orderedUrls.mapIndexed { index, url ->
-                customSetIdFromUrl(url) to index
-            }.toMap()
-            gateway.batchSetCustomSetSortOrders(orders)
-            notifyConfigChanged()
+            val orders =
+                orderedUrls.mapIndexed { index, url -> customSetIdFromUrl(url) to index }.toMap()
+            gateway.batchSetCustomSetSortOrders(orders); notifyConfigChanged()
         }
         }
     }
     }
 
 
-    /** 获取指定集内的模块(sourceUrl 可以是书源 URL 或 custom://xxx) */
     fun getJoinedModules(sourceUrl: String): List<HomepageModuleManageUi> {
     fun getJoinedModules(sourceUrl: String): List<HomepageModuleManageUi> {
-        val isSet = isCustomSetUrl(sourceUrl)
+        val isSet = isCustomSetUrl(sourceUrl);
         val setId = if (isSet) customSetIdFromUrl(sourceUrl) else null
         val setId = if (isSet) customSetIdFromUrl(sourceUrl) else null
-        val dbModules = if (isSet) {
-            allModulesCache.value.filter { it.customSetId == setId }
-        } else {
-            allModulesCache.value.filter { it.sourceUrl == sourceUrl }
-        }
+        val dbModules =
+            if (isSet) allModulesCache.value.filter { it.customSetId == setId } else allModulesCache.value.filter { it.sourceUrl == sourceUrl }
         val dbIds = dbModules.map { it.id }.toSet()
         val dbIds = dbModules.map { it.id }.toSet()
         val pendingModules = _pendingUserModules.value.filter { pending ->
         val pendingModules = _pendingUserModules.value.filter { pending ->
             val matches =
             val matches =
@@ -653,67 +628,70 @@ class HomepageViewModel(
         return (dbModules + pendingModules).map { uiFromModule(it) }
         return (dbModules + pendingModules).map { uiFromModule(it) }
     }
     }
 
 
-    /** 所有已添加的模块,按书源分组(用于自定义集添加模块) */
-    fun getAllModulesGroupedBySource(): Map<String, List<HomepageModuleManageUi>> {
-        return allModulesCache.value
-            .distinctBy { it.sourceUrl to it.moduleKey }
-            .map { uiFromModule(it) }
+    fun getAllModulesGroupedBySource(): Map<String, List<HomepageModuleManageUi>> =
+        allModulesCache.value.distinctBy { it.sourceUrl to it.moduleKey }.map { uiFromModule(it) }
             .groupBy { it.sourceUrl }
             .groupBy { it.sourceUrl }
-    }
 
 
-    fun getSourceName(sourceUrl: String): String {
-        return _bookSourcesCache.value[sourceUrl]?.bookSourceName ?: sourceUrl
-    }
+    fun getSourceName(sourceUrl: String): String =
+        _bookSourcesCache.value[sourceUrl]?.bookSourceName ?: sourceUrl
 
 
     fun assignModuleToCustomSet(moduleId: String, customSetId: String?) {
     fun assignModuleToCustomSet(moduleId: String, customSetId: String?) {
         viewModelScope.launch {
         viewModelScope.launch {
             val existing = gateway.getById(moduleId) ?: return@launch
             val existing = gateway.getById(moduleId) ?: return@launch
             if (customSetId == null) {
             if (customSetId == null) {
-                // 如果是取消分配,且它不是归属于书源默认集的,则直接删除该副本
-                if (existing.customSetId != "src_${existing.sourceUrl}") {
-                    gateway.delete(moduleId)
-                }
+                if (existing.customSetId != "src_${existing.sourceUrl}") gateway.delete(moduleId)
             } else {
             } else {
-                // 核心逻辑:分配 = 复制。生成带新 setId 的 ID
+                // 校验:目标集是否已存在无限模块
+                if (isInfinite(existing.type, existing.layoutConfig)) {
+                    val hasInfinite = allModulesCache.value.any {
+                        it.customSetId == customSetId && isInfinite(it.type, it.layoutConfig)
+                    }
+                    if (hasInfinite) {
+                        _effects.emit(
+                            HomepageEffect.ShowSnackbar(
+                                getApplication<Application>().getString(
+                                    R.string.homepage_module_duplicate_infinite
+                                )
+                            )
+                        )
+                        return@launch
+                    }
+                }
                 val newId =
                 val newId =
                     ModuleDef.globalIdOf(existing.sourceUrl, existing.moduleKey, customSetId)
                     ModuleDef.globalIdOf(existing.sourceUrl, existing.moduleKey, customSetId)
-                val newModule = existing.copy(
-                    id = newId,
-                    customSetId = customSetId,
-                    isEnabled = true, // 分配到新集时默认开启
-                    isUserCreated = true // 标记为用户创建,避免被同步清理
+                gateway.upsertAll(
+                    listOf(
+                        existing.copy(
+                            id = newId,
+                            customSetId = customSetId,
+                            isEnabled = true,
+                            isUserCreated = true
+                        )
+                    )
                 )
                 )
-                gateway.upsertAll(listOf(newModule))
             }
             }
             notifyConfigChanged()
             notifyConfigChanged()
         }
         }
     }
     }
 
 
     fun syncSourceModules(sourceUrl: String) {
     fun syncSourceModules(sourceUrl: String) {
-        viewModelScope.launch {
-            resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) }
-        }
+        viewModelScope.launch { resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) } }
     }
     }
 
 
-    /** 「书源模块」tab:仅 JSON,纯参考 */
     fun getSourceModules(
     fun getSourceModules(
         sourceUrl: String,
         sourceUrl: String,
         targetSetId: String? = null
         targetSetId: String? = null
     ): List<HomepageModuleManageUi> {
     ): List<HomepageModuleManageUi> {
         val source = resolveBookSource(sourceUrl) ?: return emptyList()
         val source = resolveBookSource(sourceUrl) ?: return emptyList()
-
         val json = source.homepageModules ?: return emptyList()
         val json = source.homepageModules ?: return emptyList()
-        val jsonDefs = parseBookSourceModules(source, json)
-
+        val jsonDefs = parseModuleDefs(source, json)
         val effectiveSetId = targetSetId ?: "src_$sourceUrl"
         val effectiveSetId = targetSetId ?: "src_$sourceUrl"
-        val joinedKeys = allModulesCache.value
-            .filter { it.sourceUrl == sourceUrl && it.customSetId == effectiveSetId }
-            .map { it.moduleKey }.toSet()
-
+        val joinedKeys =
+            allModulesCache.value.filter { it.sourceUrl == sourceUrl && it.customSetId == effectiveSetId }
+                .map { it.moduleKey }.toSet()
         return jsonDefs.map { def ->
         return jsonDefs.map { def ->
-            val id = ModuleDef.globalIdOf(sourceUrl, def.key, effectiveSetId)
             HomepageModuleManageUi(
             HomepageModuleManageUi(
-                id = id,
+                id = ModuleDef.globalIdOf(sourceUrl, def.key, effectiveSetId),
                 sourceUrl = def.sourceUrl,
                 sourceUrl = def.sourceUrl,
                 moduleKey = def.key,
                 moduleKey = def.key,
                 title = def.title,
                 title = def.title,
@@ -727,15 +705,13 @@ class HomepageViewModel(
         }
         }
     }
     }
 
 
-    /** 从书源模块「加入」→ 写入 DB,自动归属到该书源的集 */
-    /** 从发现页 Kind 创建一个 ButtonGroup 模块,args 存储选中 Kind 标题的 JSON 数组 */
     fun addButtonGroupFromKinds(
     fun addButtonGroupFromKinds(
         sourceUrl: String,
         sourceUrl: String,
         targetSetId: String?,
         targetSetId: String?,
         title: String,
         title: String,
         kindTitles: List<String>
         kindTitles: List<String>
     ) {
     ) {
-        val key = kindTitles.firstOrNull() ?: title
+        val key = kindTitles.firstOrNull() ?: title;
         val setId = targetSetId ?: "src_$sourceUrl"
         val setId = targetSetId ?: "src_$sourceUrl"
         val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
         val id = ModuleDef.globalIdOf(sourceUrl, key, setId)
         val module = ModuleItem(
         val module = ModuleItem(
@@ -759,52 +735,8 @@ class HomepageViewModel(
         }
         }
     }
     }
 
 
-    fun joinModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) {
-        val setId = targetSetId ?: "src_$sourceUrl"
-
-        if (isInfinite(def.type, def.layoutConfig)) {
-            val hasInfinite = allModulesCache.value.any {
-                it.customSetId == setId && isInfinite(
-                    it.type,
-                    it.layoutConfig
-                )
-            }
-            if (hasInfinite) {
-                viewModelScope.launch {
-                    _effects.emit(
-                        HomepageEffect.ShowSnackbar(
-                            getApplication<Application>().getString(
-                                R.string.homepage_module_duplicate_infinite
-                            )
-                        )
-                    )
-                }
-                return
-            }
-        }
-
-        val id = ModuleDef.globalIdOf(sourceUrl, def.key, setId)
-        val module = ModuleItem(
-            id = id,
-            sourceUrl = sourceUrl,
-            moduleKey = def.key,
-            type = def.type,
-            title = def.title,
-            args = def.args,
-            layoutConfig = def.layoutConfig,
-            url = def.url,
-            isEnabled = true,
-            customSetId = setId,
-            syncedAt = System.currentTimeMillis(),
-        )
-        viewModelScope.launch {
-            val source = bookSourceRepository.getBookSource(sourceUrl)
-            if (source != null) ensureSetForSource(sourceUrl, source.bookSourceName)
-            gateway.upsertAll(listOf(module))
-            _pendingUserModules.update { list -> if (list.any { it.id == id }) list else list + module }
-            notifyConfigChanged()
-        }
-    }
+    fun joinModule(sourceUrl: String, targetSetId: String?, def: ModuleDef) =
+        addCustomModule(sourceUrl, targetSetId, def)
 
 
     private fun uiFromModule(module: ModuleItem) = HomepageModuleManageUi(
     private fun uiFromModule(module: ModuleItem) = HomepageModuleManageUi(
         id = module.id,
         id = module.id,
@@ -822,27 +754,19 @@ class HomepageViewModel(
     )
     )
 
 
     fun createCustomSet(name: String) {
     fun createCustomSet(name: String) {
-        viewModelScope.launch {
-            gateway.createCustomSet(name)
-            notifyConfigChanged()
-        }
+        viewModelScope.launch { gateway.createCustomSet(name); notifyConfigChanged() }
     }
     }
 
 
     fun renameCustomSet(id: String, name: String) {
     fun renameCustomSet(id: String, name: String) {
-        viewModelScope.launch {
-            gateway.renameCustomSet(id, name)
-            notifyConfigChanged()
-        }
+        viewModelScope.launch { gateway.renameCustomSet(id, name); notifyConfigChanged() }
     }
     }
-
     fun deleteCustomSet(id: String) {
     fun deleteCustomSet(id: String) {
         viewModelScope.launch {
         viewModelScope.launch {
-            val moduleIds = allModulesCache.value.filter { it.customSetId == id }.map { it.id }
+            val ids = allModulesCache.value.filter { it.customSetId == id }.map { it.id }
             gateway.deleteCustomSet(id)
             gateway.deleteCustomSet(id)
-            moduleIds.forEach { mid ->
-                _moduleContentStates.update { it - mid }
-                loadJobs.remove(mid)?.cancel()
-                _pendingEnabled.update { it - mid }
+            ids.forEach { mid ->
+                _moduleContentStates.update { it - mid }; loadJobs.remove(mid)
+                ?.cancel(); _pendingEnabled.update { it - mid }
             }
             }
             notifyConfigChanged()
             notifyConfigChanged()
         }
         }
@@ -853,12 +777,12 @@ class HomepageViewModel(
             saveSearchBooksUseCase.save(book)
             saveSearchBooksUseCase.save(book)
             _effects.emit(
             _effects.emit(
                 HomepageEffect.NavigateToBookInfo(
                 HomepageEffect.NavigateToBookInfo(
-                    name = book.name,
-                    author = book.author,
-                    bookUrl = book.bookUrl,
-                    origin = book.origin,
-                    coverPath = book.coverUrl,
-                    sharedCoverKey = sharedCoverKey
+                    book.name,
+                    book.author,
+                    book.bookUrl,
+                    book.origin,
+                    book.coverUrl,
+                    sharedCoverKey
                 )
                 )
             )
             )
         }
         }
@@ -866,14 +790,18 @@ class HomepageViewModel(
 
 
     fun onModuleHeaderClick(sourceUrl: String, exploreUrl: String?, title: String?) {
     fun onModuleHeaderClick(sourceUrl: String, exploreUrl: String?, title: String?) {
         viewModelScope.launch {
         viewModelScope.launch {
-            _effects.emit(HomepageEffect.NavigateToExploreShow(title, sourceUrl, exploreUrl))
+            _effects.emit(
+                HomepageEffect.NavigateToExploreShow(
+                    title,
+                    sourceUrl,
+                    exploreUrl
+                )
+            )
         }
         }
     }
     }
 
 
-    private fun resolveBookSource(sourceUrl: String): BookSource? {
-        return _bookSourcesCache.value[sourceUrl]
-            ?: bookSourceRepository.getBookSourceSync(sourceUrl)
-    }
+    private fun resolveBookSource(sourceUrl: String): BookSource? =
+        _bookSourcesCache.value[sourceUrl] ?: bookSourceRepository.getBookSourceSync(sourceUrl)
 
 
     private suspend fun ensureModuleInDb(
     private suspend fun ensureModuleInDb(
         sourceUrl: String,
         sourceUrl: String,
@@ -883,8 +811,7 @@ class HomepageViewModel(
     ) {
     ) {
         if (gateway.getById(id) != null) return
         if (gateway.getById(id) != null) return
         val source = resolveBookSource(sourceUrl) ?: return
         val source = resolveBookSource(sourceUrl) ?: return
-        val json = source.homepageModules ?: return
-        val defs = parseBookSourceModules(source, json)
+        val defs = parseModuleDefs(source, source.homepageModules ?: return)
         val def = defs.find { it.key == moduleKey } ?: return
         val def = defs.find { it.key == moduleKey } ?: return
         gateway.upsertAll(
         gateway.upsertAll(
             listOf(
             listOf(
@@ -906,10 +833,6 @@ class HomepageViewModel(
     private fun notifyConfigChanged() {
     private fun notifyConfigChanged() {
         _configVersion.update { it + 1 }
         _configVersion.update { it + 1 }
     }
     }
-
-    private fun parseBookSourceModules(source: BookSource, json: String): List<ModuleDef> =
-        parseModuleDefs(source, json)
-
 }
 }
 
 
 private data class HomepageUiFlags(
 private data class HomepageUiFlags(

+ 185 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/AddCustomModuleDialog.kt

@@ -0,0 +1,185 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.google.gson.JsonParser
+import io.legado.app.R
+import io.legado.app.domain.model.HomepageModuleType
+import io.legado.app.domain.model.ModuleDef
+import io.legado.app.ui.main.homepage.HomepageViewModel
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.JsonConfigEditor
+import io.legado.app.ui.widget.components.JsonRawEditor
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.card.GlassCard
+import io.legado.app.ui.widget.components.divider.PillHeaderDivider
+import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
+import io.legado.app.ui.widget.components.text.AppText
+
+data class AddDialogPrefill(
+    val title: String = "",
+    val url: String = "",
+    val type: String = "card"
+)
+
+@Composable
+fun <T> AddCustomModuleDialog(
+    data: T?,
+    sourceUrl: String = "",
+    targetSetId: String = "",
+    prefillTitle: String = "",
+    prefillUrl: String = "",
+    prefillType: String = "card",
+    prefillArgs: String = "",
+    prefillLayoutConfig: String = "",
+    canSelectInfinite: Boolean = true,
+    onDismissRequest: () -> Unit,
+    onConfirm: (ModuleDef) -> Unit,
+) {
+    var title by remember(data) { mutableStateOf(prefillTitle) }
+    var url by remember(data) { mutableStateOf(prefillUrl) }
+    var type by remember(data) { mutableStateOf(prefillType) }
+    var args by remember(data) { mutableStateOf(prefillArgs) }
+    var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) }
+    var showRawLayoutConfig by remember(data) { mutableStateOf(false) }
+
+    val hasVisualizableKeys = remember(layoutConfig) {
+        runCatching {
+            val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject
+            jsonObject.keySet().any { key ->
+                key == "columns" || key == "rows"
+            }
+        }.getOrElse { false }
+    }
+
+    AppAlertDialog(
+        data = data,
+        onDismissRequest = onDismissRequest,
+        title = if (prefillTitle.isEmpty()) stringResource(R.string.homepage_add_module) else stringResource(
+            R.string.homepage_edit_module
+        ),
+        content = {
+            Column(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(400.dp)
+                    .verticalScroll(rememberScrollState()),
+                verticalArrangement = Arrangement.spacedBy(8.dp),
+                horizontalAlignment = Alignment.CenterHorizontally
+            ) {
+                AppTextField(
+                    value = title,
+                    onValueChange = { title = it },
+                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
+                    label = stringResource(R.string.homepage_title_label),
+                    modifier = Modifier.fillMaxWidth()
+                )
+                AppTextField(
+                    value = url,
+                    onValueChange = { url = it },
+                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
+                    label = "URL",
+                    modifier = Modifier.fillMaxWidth()
+                )
+                val typeList = remember(canSelectInfinite) {
+                    HomepageModuleType.entries.filter {
+                        it != HomepageModuleType.Unknown && (canSelectInfinite || !HomepageViewModel.isInfinite(
+                            it.key,
+                            null
+                        ))
+                    }
+                }
+
+                GlassCard(
+                    containerColor = LegadoTheme.colorScheme.onSheetContent
+                ) {
+                    DropdownListSettingItem(
+                        title = stringResource(R.string.homepage_type_label),
+                        selectedValue = type,
+                        displayEntries = typeList.map { it.title }.toTypedArray(),
+                        entryValues = typeList.map { it.key }.toTypedArray(),
+                        onValueChange = { type = it }
+                    )
+                }
+
+                if (HomepageViewModel.isInfinite(type, null) && !canSelectInfinite) {
+                    AppText(
+                        text = stringResource(R.string.homepage_module_duplicate_infinite),
+                        color = LegadoTheme.colorScheme.error,
+                        style = LegadoTheme.typography.bodySmall,
+                        modifier = Modifier.padding(horizontal = 16.dp)
+                    )
+                }
+
+                AppTextField(
+                    value = args,
+                    onValueChange = { args = it },
+                    backgroundColor = LegadoTheme.colorScheme.onSheetContent,
+                    label = "Args (JSON)",
+                    modifier = Modifier.fillMaxWidth()
+                )
+
+                PillHeaderDivider(
+                    title = stringResource(R.string.homepage_layout_config_label)
+                )
+
+                if (hasVisualizableKeys) {
+                    JsonConfigEditor(
+                        jsonString = layoutConfig,
+                        onJsonStringChange = { layoutConfig = it },
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                    CompactClickableSettingItem(
+                        title = stringResource(R.string.homepage_edit_raw_json),
+                        onClick = { showRawLayoutConfig = !showRawLayoutConfig }
+                    )
+                    if (showRawLayoutConfig) {
+                        JsonRawEditor(
+                            value = layoutConfig,
+                            onValueChange = { layoutConfig = it },
+                            label = "LayoutConfig (JSON) RAW",
+                            modifier = Modifier.fillMaxWidth()
+                        )
+                    }
+                } else {
+                    JsonRawEditor(
+                        value = layoutConfig,
+                        onValueChange = { layoutConfig = it },
+                        label = "LayoutConfig (JSON)",
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+            }
+        },
+        onConfirm = {
+            onConfirm(
+                ModuleDef(
+                    title = title,
+                    url = url,
+                    type = type,
+                    args = args,
+                    layoutConfig = layoutConfig
+                )
+            )
+        },
+        confirmText = stringResource(R.string.dialog_confirm),
+        dismissText = stringResource(R.string.dialog_cancel),
+        onDismiss = onDismissRequest
+    )
+}

+ 44 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/BrowseSourcesPage.kt

@@ -0,0 +1,44 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.ui.main.homepage.HomepageModuleManageUi
+import io.legado.app.ui.main.homepage.HomepageSourceManageUi
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.SelectionItemCard
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun BrowseSourcesPage(
+    browseSources: ImmutableList<HomepageSourceManageUi>,
+    getSourceModules: (String, String?) -> List<HomepageModuleManageUi>,
+    onSelectSource: (String) -> Unit,
+) {
+    val sources = remember(browseSources) {
+        browseSources.distinctBy { it.sourceUrl }
+    }
+    LazyColumn(
+        modifier = Modifier.fillMaxWidth(),
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        items(sources, key = { it.sourceUrl }) { source ->
+            val moduleCount = getSourceModules(source.sourceUrl, null).size
+            SelectionItemCard(
+                title = source.sourceName,
+                subtitle = stringResource(R.string.homepage_n_modules, moduleCount),
+                containerColor = LegadoTheme.colorScheme.onSheetContent,
+                onToggleSelection = {
+                    onSelectSource(source.sourceUrl)
+                }
+            )
+        }
+    }
+}

+ 90 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/CustomSetAddModulesPage.kt

@@ -0,0 +1,90 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.ui.main.homepage.HomepageModuleManageUi
+import io.legado.app.ui.main.homepage.HomepageViewModel
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.SelectionItemCard
+import io.legado.app.ui.widget.components.text.AppText
+import kotlinx.collections.immutable.ImmutableList
+
+@Composable
+fun CustomSetAddModulesPage(
+    setUrl: String,
+    allJoinedModules: ImmutableList<HomepageModuleManageUi>,
+    sourceNames: Map<String, String>,
+    onToggleModuleToSet: (HomepageModuleManageUi, inCurrentSet: Boolean, isBlocked: Boolean) -> Unit,
+) {
+    val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
+    val initialJoined = allJoinedModules
+        .filter { it.customSetId == setId }
+        .associateBy({ it.moduleKey }, { it.id })
+    var joinedInCurrent by remember(initialJoined) { mutableStateOf(initialJoined) }
+
+    val hasInfiniteInCurrentSet = remember(allJoinedModules) {
+        allJoinedModules.any {
+            it.customSetId == setId && HomepageViewModel.isInfinite(
+                it.type,
+                it.layoutConfig
+            )
+        }
+    }
+
+    val grouped = remember(allJoinedModules) {
+        allJoinedModules
+            .distinctBy { it.sourceUrl to it.moduleKey }
+            .groupBy { it.sourceUrl }
+    }
+
+    LazyColumn(
+        modifier = Modifier.fillMaxWidth(),
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        grouped.forEach { (sourceUrl, modules) ->
+            item(key = "header_$sourceUrl") {
+                AppText(
+                    text = sourceNames[sourceUrl] ?: sourceUrl,
+                    style = LegadoTheme.typography.labelLarge,
+                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
+                )
+            }
+            items(modules, key = { it.id }) { module ->
+                val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
+                val inCurrentSet = instanceIdInCurrentSet != null
+                val isInfinite = HomepageViewModel.isInfinite(module.type, module.layoutConfig)
+                val isBlocked = !inCurrentSet && isInfinite && hasInfiniteInCurrentSet
+
+                SelectionItemCard(
+                    title = module.title,
+                    subtitle = module.moduleKey + if (isBlocked) " (${stringResource(R.string.homepage_module_duplicate_infinite)})" else "",
+                    containerColor = LegadoTheme.colorScheme.onSheetContent,
+                    isSelected = inCurrentSet,
+                    inSelectionMode = true,
+                    isEnabled = !isBlocked,
+                    onToggleSelection = {
+                        onToggleModuleToSet(module, inCurrentSet, isBlocked)
+                        if (inCurrentSet) {
+                            joinedInCurrent = joinedInCurrent - module.moduleKey
+                        } else if (!isBlocked) {
+                            joinedInCurrent =
+                                joinedInCurrent + (module.moduleKey to "temp_${module.id}")
+                        }
+                    }
+                )
+            }
+        }
+    }
+}

+ 188 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetDetailPage.kt

@@ -0,0 +1,188 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.domain.model.HomepageModuleType
+import io.legado.app.ui.main.homepage.HomepageModuleManageUi
+import io.legado.app.ui.main.homepage.HomepageViewModel
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.SecondaryButton
+import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
+import io.legado.app.ui.widget.components.card.SelectionItemCard
+import io.legado.app.ui.widget.components.divider.PillDivider
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.utils.move
+import kotlinx.collections.immutable.ImmutableList
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+@Composable
+fun SetDetailPage(
+    setUrl: String,
+    allJoinedModules: ImmutableList<HomepageModuleManageUi>,
+    onToggleModule: (String, Boolean) -> Unit,
+    onReorderModules: (List<String>) -> Unit,
+    onEditModule: (HomepageModuleManageUi) -> Unit,
+    onRequestDeleteModule: (String) -> Unit,
+    onBrowseSourceModules: () -> Unit,
+    onAddModules: () -> Unit,
+) {
+    val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
+    val modules = remember(setId, allJoinedModules) {
+        allJoinedModules.filter { it.customSetId == setId }.distinctBy { it.id }
+    }
+
+    val standardModules = remember(modules) {
+        modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+    }
+    val infiniteModules = remember(modules) {
+        modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+    }
+
+    if (modules.isEmpty()) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(24.dp),
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            AppText(stringResource(R.string.homepage_no_modules))
+            SecondaryButton(
+                text = stringResource(R.string.homepage_browse_to_add),
+                onClick = {
+                    if (setId.startsWith("src_")) onBrowseSourceModules()
+                    else onAddModules()
+                }
+            )
+        }
+    } else {
+        var listData by remember(setUrl, standardModules) {
+            mutableStateOf(standardModules)
+        }
+        val listState = rememberLazyListState()
+        val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
+            listData = listData.toMutableList().apply {
+                if (isEmpty()) return@apply
+                val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
+                val toIndex = (to.index - 1).coerceIn(0, lastIndex)
+                if (fromIndex in indices && toIndex in indices) {
+                    move(fromIndex, toIndex)
+                }
+            }
+        }
+
+        LaunchedEffect(standardModules) {
+            if (!reorderableState.isAnyItemDragging) listData = standardModules
+        }
+        LaunchedEffect(reorderableState.isAnyItemDragging) {
+            if (!reorderableState.isAnyItemDragging) {
+                val orderedIds =
+                    (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
+                if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
+            }
+        }
+
+        LazyColumn(
+            state = listState,
+            modifier = Modifier.fillMaxWidth(),
+            verticalArrangement = Arrangement.spacedBy(8.dp)
+        ) {
+            if (listData.isNotEmpty()) {
+                item(key = "header_std_detail") {
+                    AppText(
+                        text = stringResource(R.string.homepage_standard_module),
+                        style = LegadoTheme.typography.labelMedium,
+                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
+                    )
+                }
+                items(listData, key = { it.id }) { module ->
+                    ReorderableSelectionItem(
+                        state = reorderableState,
+                        key = module.id,
+                        title = module.title,
+                        subtitle = HomepageModuleType.fromKey(module.type).title,
+                        isEnabled = module.isVisible,
+                        containerColor = LegadoTheme.colorScheme.onSheetContent,
+                        onEnabledChange = { enabled ->
+                            onToggleModule(module.id, enabled)
+                            listData = listData.map {
+                                if (it.id == module.id) it.copy(isVisible = enabled) else it
+                            }
+                        },
+                        trailingAction = {
+                            SmallIconButton(
+                                onClick = { onEditModule(module) },
+                                imageVector = Icons.Default.Edit
+                            )
+                            SmallIconButton(
+                                onClick = { onRequestDeleteModule(module.id) },
+                                imageVector = Icons.Default.Delete
+                            )
+                        }
+                    )
+                }
+            }
+
+            if (infiniteModules.isNotEmpty()) {
+                item(key = "header_inf_detail") {
+                    PillDivider()
+                    AppText(
+                        text = stringResource(R.string.homepage_infinite_module_slot),
+                        style = LegadoTheme.typography.labelMedium,
+                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
+                    )
+                }
+                items(infiniteModules, key = { it.id }) { module ->
+                    val isEffective = infiniteModules.firstOrNull() == module
+                    SelectionItemCard(
+                        title = module.title,
+                        subtitle = HomepageModuleType.fromKey(module.type).title,
+                        isEnabled = module.isVisible,
+                        containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
+                        onEnabledChange = { onToggleModule(module.id, it) },
+                        trailingAction = {
+                            SmallIconButton(
+                                onClick = { onEditModule(module) },
+                                imageVector = Icons.Default.Edit
+                            )
+                            SmallIconButton(
+                                onClick = { onRequestDeleteModule(module.id) },
+                                imageVector = Icons.Default.Delete
+                            )
+                        }
+                    )
+                }
+            }
+
+            item(key = "browse_from_set") {
+                SecondaryButton(
+                    text = stringResource(R.string.homepage_browse_source_modules),
+                    onClick = {
+                        if (setId.startsWith("src_")) onBrowseSourceModules()
+                        else onAddModules()
+                    },
+                    modifier = Modifier.fillMaxWidth()
+                )
+            }
+        }
+    }
+}

+ 119 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/SetListPage.kt

@@ -0,0 +1,119 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.DriveFileRenameOutline
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.ui.main.homepage.HomepageSourceManageUi
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.SecondaryButton
+import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
+import io.legado.app.ui.widget.components.divider.PillDivider
+import io.legado.app.utils.move
+import kotlinx.collections.immutable.ImmutableList
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+@Composable
+fun SetListPage(
+    sets: ImmutableList<HomepageSourceManageUi>,
+    onToggleSet: (String, Boolean) -> Unit,
+    onReorderSets: (List<String>) -> Unit,
+    onSelectSet: (String) -> Unit,
+    onRenameSet: (String) -> Unit,
+    onDeleteSet: (String) -> Unit,
+    onCreateSet: () -> Unit,
+    onBrowseSources: () -> Unit,
+) {
+    var localSets by remember { mutableStateOf(sets.distinctBy { it.sourceUrl }) }
+    val setsListState = rememberLazyListState()
+    val setsReorderableState =
+        rememberReorderableLazyListState(setsListState) { from, to ->
+            localSets = localSets.toMutableList().apply {
+                if (isEmpty()) return@apply
+                val fromIndex = from.index.coerceIn(0, lastIndex)
+                val toIndex = to.index.coerceIn(0, lastIndex)
+                if (fromIndex in indices && toIndex in indices) {
+                    move(fromIndex, toIndex)
+                }
+            }
+        }
+
+    LaunchedEffect(sets) {
+        if (!setsReorderableState.isAnyItemDragging) localSets = sets.distinctBy { it.sourceUrl }
+    }
+    LaunchedEffect(setsReorderableState.isAnyItemDragging) {
+        if (!setsReorderableState.isAnyItemDragging) {
+            val orderedUrls = localSets.map { it.sourceUrl }
+            if (orderedUrls != sets.map { it.sourceUrl }) onReorderSets(orderedUrls)
+        }
+    }
+
+    LazyColumn(
+        state = setsListState,
+        modifier = Modifier.fillMaxWidth(),
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        items(localSets, key = { it.sourceUrl }) { set ->
+            ReorderableSelectionItem(
+                state = setsReorderableState,
+                key = set.sourceUrl,
+                title = set.sourceName,
+                subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
+                containerColor = LegadoTheme.colorScheme.onSheetContent,
+                isEnabled = set.isSelected,
+                onToggleSelection = { onSelectSet(set.sourceUrl) },
+                onEnabledChange = { enabled ->
+                    onToggleSet(set.sourceUrl, enabled)
+                    localSets = localSets.map {
+                        if (it.sourceUrl == set.sourceUrl) it.copy(isSelected = enabled) else it
+                    }
+                },
+                trailingAction = {
+                    SmallIconButton(
+                        onClick = { onRenameSet(set.sourceUrl) },
+                        imageVector = Icons.Default.DriveFileRenameOutline
+                    )
+                    SmallIconButton(
+                        onClick = { onDeleteSet(set.sourceUrl) },
+                        imageVector = Icons.Default.Delete
+                    )
+                }
+            )
+        }
+        item {
+            PillDivider(
+                modifier = Modifier.padding(vertical = 12.dp)
+            )
+        }
+        item(key = "create_set") {
+            SecondaryButton(
+                text = stringResource(R.string.homepage_new_custom_set),
+                onClick = onCreateSet,
+                modifier = Modifier.fillMaxWidth()
+            )
+        }
+        item(key = "browse_sources") {
+            SecondaryButton(
+                text = stringResource(R.string.homepage_browse_source_modules),
+                onClick = onBrowseSources,
+                modifier = Modifier.fillMaxWidth()
+            )
+        }
+    }
+}

+ 383 - 0
app/src/main/java/io/legado/app/ui/main/homepage/manage/SourceBrowseDetailPage.kt

@@ -0,0 +1,383 @@
+package io.legado.app.ui.main.homepage.manage
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.domain.model.HomepageModuleType
+import io.legado.app.domain.model.ModuleDef
+import io.legado.app.ui.main.homepage.HomepageModuleManageUi
+import io.legado.app.ui.main.homepage.HomepageViewModel
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.SecondaryButton
+import io.legado.app.ui.widget.components.button.SmallIconButton
+import io.legado.app.ui.widget.components.card.GlassCard
+import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
+import io.legado.app.ui.widget.components.card.SelectionItemCard
+import io.legado.app.ui.widget.components.divider.PillDivider
+import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet
+import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
+import io.legado.app.ui.widget.components.tabRow.AppTabRow
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.utils.move
+import kotlinx.collections.immutable.ImmutableList
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SourceBrowseDetailPage(
+    browseUrl: String,
+    selectingSetUrl: String?,
+    allJoinedModules: ImmutableList<HomepageModuleManageUi>,
+    canSelectInfiniteGlobal: Boolean,
+    onGetSourceModules: (String, String?) -> List<HomepageModuleManageUi>,
+    onGetExploreKinds: (String) -> List<Pair<String, String>>,
+    onToggleModule: (String, Boolean) -> Unit,
+    onJoinModule: (String, String?, ModuleDef) -> Unit,
+    onRequestDeleteModule: (String) -> Unit,
+    onReorderModules: (List<String>) -> Unit,
+    onEditModule: (HomepageModuleManageUi) -> Unit,
+    onAddDialogPrefill: (AddDialogPrefill?) -> Unit,
+    onShowAddButtonGroupDialog: () -> Unit,
+    browseTab: Int,
+    onBrowseTabChange: (Int) -> Unit,
+    browseModuleType: String,
+    onBrowseModuleTypeChange: (String) -> Unit,
+    selectedKindTitles: Set<String>,
+    onSelectedKindTitlesChange: (Set<String>) -> Unit,
+) {
+    var showKindSelect by remember { mutableStateOf(false) }
+
+    val displaySetUrl =
+        selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
+    val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
+
+    val joinedModules = remember(displaySetUrl, allJoinedModules) {
+        allJoinedModules.filter { it.customSetId == currentSetId }
+    }
+
+    val standardModules = remember(joinedModules) {
+        joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+    }
+    val infiniteModules = remember(joinedModules) {
+        joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+    }
+    val hasInfiniteInSet = infiniteModules.isNotEmpty()
+
+    val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
+    val sourceModules = onGetSourceModules(browseUrl, currentSetId)
+    Column {
+        AppTabRow(
+            tabTitles = listOf(
+                stringResource(R.string.homepage_tab_joined),
+                stringResource(R.string.homepage_tab_source_modules),
+                stringResource(R.string.homepage_tab_discover)
+            ),
+            selectedTabIndex = browseTab,
+            onTabSelected = { onBrowseTabChange(it) }
+        )
+        when (browseTab) {
+            0 -> {
+                if (joinedModules.isEmpty()) {
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(24.dp),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        AppText(stringResource(R.string.homepage_no_joined_modules))
+                    }
+                } else {
+                    var listData by remember(displaySetUrl, standardModules) {
+                        mutableStateOf(standardModules.distinctBy { it.id })
+                    }
+                    val listState = rememberLazyListState()
+                    val reorderableState =
+                        rememberReorderableLazyListState(listState) { from, to ->
+                            listData = listData.toMutableList().apply {
+                                if (isEmpty()) return@apply
+                                val fromIndex = (from.index - 1).coerceIn(0, lastIndex)
+                                val toIndex = (to.index - 1).coerceIn(0, lastIndex)
+                                if (fromIndex in indices && toIndex in indices) {
+                                    move(fromIndex, toIndex)
+                                }
+                            }
+                        }
+                    LaunchedEffect(standardModules) {
+                        if (!reorderableState.isAnyItemDragging) listData =
+                            standardModules.distinctBy { it.id }
+                    }
+                    LaunchedEffect(reorderableState.isAnyItemDragging) {
+                        if (!reorderableState.isAnyItemDragging) {
+                            val orderedIds =
+                                (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
+                            if (orderedIds != joinedModules.map { it.id }) {
+                                onReorderModules(orderedIds)
+                            }
+                        }
+                    }
+                    LazyColumn(
+                        state = listState,
+                        modifier = Modifier.fillMaxWidth(),
+                        verticalArrangement = Arrangement.spacedBy(8.dp)
+                    ) {
+                        item(key = "header_standard") {
+                            AppText(
+                                text = stringResource(R.string.homepage_standard_modules_sortable),
+                                style = LegadoTheme.typography.labelMedium,
+                                color = LegadoTheme.colorScheme.primary,
+                                modifier = Modifier.padding(
+                                    horizontal = 16.dp,
+                                    vertical = 4.dp
+                                )
+                            )
+                        }
+
+                        items(listData, key = { it.id }) { module ->
+                            ReorderableSelectionItem(
+                                state = reorderableState,
+                                key = module.id,
+                                title = module.title,
+                                subtitle = HomepageModuleType.fromKey(module.type).title,
+                                isEnabled = module.isVisible,
+                                containerColor = LegadoTheme.colorScheme.onSheetContent,
+                                onEnabledChange = { enabled ->
+                                    onToggleModule(module.id, enabled)
+                                    listData = listData.map {
+                                        if (it.id == module.id) it.copy(isVisible = enabled) else it
+                                    }
+                                },
+                                trailingAction = {
+                                    SmallIconButton(
+                                        onClick = { onEditModule(module) },
+                                        imageVector = Icons.Default.Edit
+                                    )
+                                    SmallIconButton(
+                                        onClick = { onRequestDeleteModule(module.id) },
+                                        imageVector = Icons.Default.Delete
+                                    )
+                                }
+                            )
+                        }
+
+                        if (infiniteModules.isNotEmpty()) {
+                            item(key = "header_infinite") {
+                                PillDivider(
+                                    modifier = Modifier.padding(
+                                        vertical = 8.dp,
+                                        horizontal = 16.dp
+                                    )
+                                )
+                                AppText(
+                                    text = stringResource(R.string.homepage_infinite_module_bottom),
+                                    style = LegadoTheme.typography.labelMedium,
+                                    modifier = Modifier.padding(
+                                        horizontal = 16.dp,
+                                        vertical = 4.dp
+                                    )
+                                )
+                            }
+
+                            items(infiniteModules, key = { it.id }) { module ->
+                                val isEffective =
+                                    infiniteModules.firstOrNull() == module
+                                SelectionItemCard(
+                                    title = module.title,
+                                    subtitle = HomepageModuleType.fromKey(module.type).title + if (isEffective) stringResource(
+                                        R.string.homepage_status_in_effect
+                                    ) else stringResource(R.string.homepage_status_blocked),
+                                    isEnabled = module.isVisible,
+                                    containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
+                                    onEnabledChange = { onToggleModule(module.id, it) },
+                                    trailingAction = {
+                                        SmallIconButton(
+                                            onClick = { onEditModule(module) },
+                                            imageVector = Icons.Default.Edit
+                                        )
+                                        SmallIconButton(
+                                            onClick = { onRequestDeleteModule(module.id) },
+                                            imageVector = Icons.Default.Delete
+                                        )
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+
+            1 -> {
+                if (sourceModules.isEmpty()) {
+                    Box(
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(24.dp),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        AppText(stringResource(R.string.homepage_source_json_empty))
+                    }
+                } else {
+                    LazyColumn(
+                        modifier = Modifier.fillMaxWidth(),
+                        verticalArrangement = Arrangement.spacedBy(4.dp)
+                    ) {
+                        items(
+                            sourceModules.distinctBy { it.id },
+                            key = { it.id }) { module ->
+                            val isJoined = joinedKeys.contains(module.moduleKey)
+                            val isInfinite =
+                                HomepageViewModel.isInfinite(module.type, module.layoutConfig)
+                            val isBlocked = !isJoined && isInfinite && hasInfiniteInSet
+
+                            SelectionItemCard(
+                                title = module.title,
+                                subtitle = module.moduleKey + if (isJoined) stringResource(
+                                    R.string.homepage_status_joined
+                                ) else if (isBlocked) " (${stringResource(R.string.homepage_module_duplicate_infinite)})" else "",
+                                containerColor = LegadoTheme.colorScheme.onSheetContent,
+                                isSelected = isJoined,
+                                inSelectionMode = true,
+                                isEnabled = !isBlocked,
+                                onToggleSelection = {
+                                    if (!isJoined && !isBlocked) {
+                                        onJoinModule(
+                                            browseUrl, currentSetId, ModuleDef(
+                                                key = module.moduleKey,
+                                                type = module.type,
+                                                title = module.title,
+                                                sourceUrl = browseUrl,
+                                            )
+                                        )
+                                    }
+                                }
+                            )
+                        }
+                    }
+                }
+            }
+
+            2 -> {
+                val effectiveTargetSetId = currentSetId
+                val isButtonGroup = browseModuleType == "buttonGroup"
+                Column {
+                    val typeList = remember(canSelectInfiniteGlobal) {
+                        HomepageModuleType.entries.filter {
+                            it != HomepageModuleType.Unknown && (canSelectInfiniteGlobal || !HomepageViewModel.isInfinite(
+                                it.key,
+                                null
+                            ))
+                        }
+                    }
+
+                    LaunchedEffect(canSelectInfiniteGlobal) {
+                        if (!canSelectInfiniteGlobal && HomepageViewModel.isInfinite(
+                                browseModuleType,
+                                null
+                            )
+                        ) {
+                            onBrowseModuleTypeChange("card")
+                        }
+                    }
+
+                    GlassCard(
+                        containerColor = LegadoTheme.colorScheme.onSheetContent,
+                        cornerRadius = 12.dp
+                    ) {
+                        CompactDropdownSettingItem(
+                            title = stringResource(R.string.homepage_module_type),
+                            selectedValue = browseModuleType,
+                            displayEntries = typeList.map { it.title }.toTypedArray(),
+                            entryValues = typeList.map { it.key }.toTypedArray(),
+                            onValueChange = {
+                                onBrowseModuleTypeChange(it)
+                                onSelectedKindTitlesChange(emptySet())
+                            }
+                        )
+                    }
+
+                    Spacer(modifier = Modifier.height(16.dp))
+
+                    SelectionItemCard(
+                        title = stringResource(R.string.homepage_select_from_kinds),
+                        subtitle = if (isButtonGroup) {
+                            if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds)
+                            else stringResource(
+                                R.string.homepage_n_selected,
+                                selectedKindTitles.size
+                            )
+                        } else {
+                            stringResource(R.string.homepage_select_one_kind)
+                        },
+                        containerColor = LegadoTheme.colorScheme.onSheetContent,
+                        onToggleSelection = { showKindSelect = true },
+                        trailingAction = {
+                            if (isButtonGroup && selectedKindTitles.isNotEmpty()) {
+                                SmallIconButton(
+                                    onClick = { onShowAddButtonGroupDialog() },
+                                    imageVector = Icons.Default.Check
+                                )
+                            }
+                        }
+                    )
+
+                    ExploreKindSelectSheet(
+                        show = showKindSelect,
+                        onDismissRequest = { showKindSelect = false },
+                        sourceUrl = browseUrl,
+                        multiple = isButtonGroup,
+                        initialSelectedTitles = selectedKindTitles.toList(),
+                        onSelected = { kinds ->
+                            if (isButtonGroup) {
+                                onSelectedKindTitlesChange(kinds.map { it.title }.toSet())
+                            } else {
+                                kinds.firstOrNull()?.let { kind ->
+                                    onAddDialogPrefill(
+                                        AddDialogPrefill(
+                                            title = kind.title,
+                                            url = kind.url ?: "",
+                                            type = browseModuleType
+                                        )
+                                    )
+                                }
+                            }
+                        }
+                    )
+
+                    PillDivider(
+                        modifier = Modifier.padding(vertical = 12.dp)
+                    )
+
+                    SecondaryButton(
+                        text = stringResource(R.string.homepage_manual_add),
+                        onClick = {
+                            onAddDialogPrefill(AddDialogPrefill(type = browseModuleType))
+                        },
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+            }
+        }
+    }
+}

+ 1 - 0
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt

@@ -229,6 +229,7 @@ fun SearchBookGridItem(
                 style = LegadoTheme.typography.bodySmall,
                 style = LegadoTheme.typography.bodySmall,
                 fontWeight = FontWeight.Bold,
                 fontWeight = FontWeight.Bold,
                 maxLines = 2,
                 maxLines = 2,
+                minLines = 2,
                 overflow = TextOverflow.Ellipsis,
                 overflow = TextOverflow.Ellipsis,
             )
             )
         }
         }

+ 2 - 2
app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt

@@ -77,7 +77,7 @@ fun AppModalBottomSheet(
                     }
                     }
                 }
                 }
             },
             },
-            insideMargin = DpSize(16.dp, 12.dp),
+            insideMargin = DpSize(16.dp, 0.dp),
             backgroundColor = sheetContainerColor,
             backgroundColor = sheetContainerColor,
             dragHandleColor = sheetDragHandleColor,
             dragHandleColor = sheetDragHandleColor,
             onDismissRequest = onDismissRequest,
             onDismissRequest = onDismissRequest,
@@ -120,7 +120,7 @@ fun AppModalBottomSheet(
                     Column(
                     Column(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
-                            .padding(start = 16.dp, end = 16.dp, bottom = 24.dp)
+                            .padding(start = 16.dp, end = 16.dp, bottom = 0.dp)
                             .heightIn(max = maxHeight)
                             .heightIn(max = maxHeight)
                             .animateContentSize()
                             .animateContentSize()
                             .then(modifier)
                             .then(modifier)

+ 1 - 1
app/src/main/res/values-zh-rCN/strings.xml

@@ -1689,7 +1689,7 @@
     <string name="homepage_tab_discover">发现</string>
     <string name="homepage_tab_discover">发现</string>
     <string name="homepage_no_joined_modules">暂无已加入的模块</string>
     <string name="homepage_no_joined_modules">暂无已加入的模块</string>
     <string name="homepage_standard_modules_sortable">标准模块 (可拖拽排序)</string>
     <string name="homepage_standard_modules_sortable">标准模块 (可拖拽排序)</string>
-    <string name="homepage_infinite_module_bottom">底栏无限模块</string>
+    <string name="homepage_infinite_module_bottom">底栏模块 (无限加载,仅可存在一个)</string>
     <string name="homepage_in_effect">当前生效</string>
     <string name="homepage_in_effect">当前生效</string>
     <string name="homepage_blocked">已被屏蔽</string>
     <string name="homepage_blocked">已被屏蔽</string>
     <string name="homepage_source_json_empty">该书源的 homepageModules JSON 为空</string>
     <string name="homepage_source_json_empty">该书源的 homepageModules JSON 为空</string>

+ 13 - 49
gradle/libs.versions.toml

@@ -1,20 +1,20 @@
 [versions]
 [versions]
 
 
 accompanistWebview = "0.36.0"
 accompanistWebview = "0.36.0"
-adaptive = "1.3.0-beta01"
-adaptiveLayout = "1.3.0-beta01"
-adaptiveNavigation = "1.3.0-beta01"
-animation = "1.11.1"
+adaptive = "1.3.0-beta02"
+adaptiveLayout = "1.3.0-beta02"
+adaptiveNavigation = "1.3.0-beta02"
+animation = "1.11.2"
 biometric = "1.4.0-alpha07"
 biometric = "1.4.0-alpha07"
 coilCompose = "2.7.0"
 coilCompose = "2.7.0"
-composeBom = "2026.05.00"
+composeBom = "2026.05.01"
 constraintlayoutCompose = "1.1.1"
 constraintlayoutCompose = "1.1.1"
 coreSplashscreen = "1.2.0"
 coreSplashscreen = "1.2.0"
 datastorePreferences = "1.2.1"
 datastorePreferences = "1.2.1"
-foundation = "1.11.1"
+foundation = "1.11.2"
 kotlin = "2.3.21"
 kotlin = "2.3.21"
 kotlinxSerialization = "1.11.0"
 kotlinxSerialization = "1.11.0"
-kotlinxCoroutinesAndroid = "1.10.2"
+kotlinxCoroutinesAndroid = "1.11.0"
 kotlinxSerializationJson = "1.11.0"
 kotlinxSerializationJson = "1.11.0"
 kotlinxCollectionsImmutable = "0.4.0"
 kotlinxCollectionsImmutable = "0.4.0"
 ksp = "2.3.6"
 ksp = "2.3.6"
@@ -45,11 +45,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.1.1"
+navigation3 = "1.1.2"
 markdownRenderer = "0.40.2"
 markdownRenderer = "0.40.2"
 markwon = "4.6.2"
 markwon = "4.6.2"
-material = "1.14.0-rc01"
-material3 = "1.5.0-alpha19"
+material = "1.14.0"
+material3 = "1.5.0-alpha20"
 material3IconsExtended = "1.7.8"
 material3IconsExtended = "1.7.8"
 materialKolor = "4.1.1"
 materialKolor = "4.1.1"
 media = "1.8.0"
 media = "1.8.0"
@@ -59,7 +59,7 @@ okhttp = "5.3.2"
 palette = "1.0.0"
 palette = "1.0.0"
 preference = "1.2.1"
 preference = "1.2.1"
 protobufJavalite = "4.26.1"
 protobufJavalite = "4.26.1"
-quickChineseTransfer = "0.2.16"
+quickChineseTransfer = "0.2.17"
 room = "2.8.4"
 room = "2.8.4"
 splitties = "3.0.0"
 splitties = "3.0.0"
 rhino = "1.9.1"
 rhino = "1.9.1"
@@ -96,15 +96,10 @@ profileinstaller = "1.4.1"
 [libraries]
 [libraries]
 
 
 accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
 accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "accompanistWebview" }
-activity-activity = { module = "androidx.activity:activity", version.ref = "activity" }
 activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
 activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity" }
 activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
 activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity" }
 
 
-androidx-activity-compose = { module = "androidx.activity:activity-compose" }
-androidx-annotation = { group = "androidx.annotation", name = "annotation", version = "1.9.1" }
-#androidx-annotation-experimental = { group = "androidx.annotation", name = "annotation-experimental", version = "1.3.1" }
-
-androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "biometric" }
+androidx-annotation = { group = "androidx.annotation", name = "annotation", version = "1.10.0" }
 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 = { 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-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" }
@@ -143,7 +138,6 @@ koin-compose-viewmodel = { group = "io.insert-koin", name = "koin-compose-viewmo
 coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
 coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coilCompose" }
 coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" }
 coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coilCompose" }
 coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coilCompose" }
 coil-svg = { module = "io.coil-kt:coil-svg", version.ref = "coilCompose" }
-core-core = { module = "androidx.core:core", version.ref = "core" }
 core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
 core-ktx = { module = "androidx.core:core-ktx", version.ref = "core" }
 
 
 androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
 androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version = "3.7.0" }
@@ -153,17 +147,13 @@ firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
 firebase-perf = { module = "com.google.firebase:firebase-perf" }
 firebase-perf = { module = "com.google.firebase:firebase-perf" }
 
 
 
 
-fragment-fragment = { module = "androidx.fragment:fragment", version.ref = "fragment" }
 fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
 fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "fragment" }
 
 
 androidx-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
 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-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
 kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
-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" }
@@ -173,7 +163,6 @@ material-kolor = { module = "com.materialkolor:material-kolor", version.ref = "m
 media-media = { module = "androidx.media:media", version.ref = "media" }
 media-media = { module = "androidx.media:media", version.ref = "media" }
 media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
 media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
 media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
 media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
 
 
 markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
 markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" }
 markwon-image-glide = { module = "io.noties.markwon:image-glide", version.ref = "markwon" }
 markwon-image-glide = { module = "io.noties.markwon:image-glide", version.ref = "markwon" }
@@ -187,8 +176,6 @@ ktor-serialization-gson = { module = "io.ktor:ktor-serialization-gson", version.
 ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
 ktor-server-cors = { module = "io.ktor:ktor-server-cors", version.ref = "ktor" }
 ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
 ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
 
 
-navigation-compose = { module = "androidx.navigation:navigation-compose" }
-preference-preference = { module = "androidx.preference:preference", version.ref = "preference" }
 preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference" }
 preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference" }
 
 
 okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
 okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
@@ -204,16 +191,11 @@ room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
 glide-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
 glide-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
 glide-okhttp= { module = "com.github.bumptech.glide:okhttp3-integration", version.ref ="glide" }
 glide-okhttp= { module = "com.github.bumptech.glide:okhttp3-integration", version.ref ="glide" }
 glide-recyclerview={module="com.github.bumptech.glide:recyclerview-integration",version.ref="glide"}
 glide-recyclerview={module="com.github.bumptech.glide:recyclerview-integration",version.ref="glide"}
-glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
-glide-compose = { module = "com.github.bumptech.glide:compose", version = "1.0.0-beta08" }
 glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
 glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
-glide-avif = { module = "com.github.bumptech.glide:avif-integration", version.ref = "glide" }
-avif = { module = "org.aomedia.avif.android:avif", version = "1.3.0.841110fd" }
 glide-svg = { module = "com.github.qoqa:glide-svg", version = "4.0.2" }
 glide-svg = { module = "com.github.qoqa:glide-svg", version = "4.0.2" }
 androidsvg = { module = "com.caverock:androidsvg-aar", version = "1.4" }
 androidsvg = { module = "com.caverock:androidsvg-aar", version = "1.4" }
 
 
 splitties-appctx = { module = "com.louiscad.splitties:splitties-appctx", version.ref = "splitties" }
 splitties-appctx = { module = "com.louiscad.splitties:splitties-appctx", version.ref = "splitties" }
-splitties-activities = { module = "com.louiscad.splitties:splitties-activities", version.ref = "splitties" }
 splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
 splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
 splitties-views = { module = "com.louiscad.splitties:splitties-views", version.ref = "splitties" }
 splitties-views = { module = "com.louiscad.splitties:splitties-views", version.ref = "splitties" }
 
 
@@ -237,18 +219,12 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
 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" }
 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" }
-
 
 
-#kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
-#kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinxSerialization" }
 liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref = "liveeventbus" }
 liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref = "liveeventbus" }
 material = { module = "com.google.android.material:material", version.ref = "material" }
 material = { module = "com.google.android.material:material", version.ref = "material" }
 
 
 mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
 mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
-renderscript-intrinsics-replacement-toolkit = { module = "com.github.TomasValenta:renderscript-intrinsics-replacement-toolkit", version = "8eaa829ddd" }
 zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
 zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
-androidx-biometric-ktx = { group = "androidx.biometric", name = "biometric-ktx", version.ref = "biometricKtx" }
 
 
 reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
 reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" }
 
 
@@ -260,7 +236,6 @@ miuix-core = { module = "top.yukonga.miuix.kmp:miuix-core", version.ref = "miuix
 miuix-blur-android = { module = "top.yukonga.miuix.kmp:miuix-blur-android", version.ref = "miuix" }
 miuix-blur-android = { module = "top.yukonga.miuix.kmp:miuix-blur-android", version.ref = "miuix" }
 miuix-icons-android = { module = "top.yukonga.miuix.kmp:miuix-icons-android", version.ref = "miuix" }
 miuix-icons-android = { module = "top.yukonga.miuix.kmp:miuix-icons-android", version.ref = "miuix" }
 miuix-preference-android = { module = "top.yukonga.miuix.kmp:miuix-preference-android", version.ref = "miuix" }
 miuix-preference-android = { module = "top.yukonga.miuix.kmp:miuix-preference-android", version.ref = "miuix" }
-miuix-shapes-android = { module = "top.yukonga.miuix.kmp:miuix-shapes-android", version.ref = "miuix" }
 miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.ref = "miuix" }
 miuix-ui-android = { module = "top.yukonga.miuix.kmp:miuix-ui-android", version.ref = "miuix" }
 
 
 capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
 capsule = { module = "io.github.kyant0:capsule", version.ref = "capsule" }
@@ -280,16 +255,7 @@ android-application = { id = "com.android.application", version.ref = "agp" }
 android-library = { id = "com.android.library", version.ref = "agp" }
 android-library = { id = "com.android.library", version.ref = "agp" }
 android-test = { id = "com.android.test", version.ref = "agp" }
 android-test = { id = "com.android.test", version.ref = "agp" }
 
 
-#androidx-benchmark = { id = "androidx.benchmark", version = "1.3.3" }
-
-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
-kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
-kotlin-allopen = { id = "org.jetbrains.kotlin.plugin.allopen", version.ref = "kotlin" }
-kotlin-noarg = { id = "org.jetbrains.kotlin.plugin.noarg", version.ref = "kotlin" }
-kotlin-sam = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "kotlin" }
-kotlin-lombok = { id = "org.jetbrains.kotlin.plugin.lombok", version.ref = "kotlin" }
 kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
 kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
 
 
 google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
 google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
@@ -297,7 +263,5 @@ google-services = { id = "com.google.gms.google-services", version = "4.4.4" }
 ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 room = { id = "androidx.room", version.ref = "room" }
 room = { id = "androidx.room", version.ref = "room" }
 
 
-#protobuf = { id = "com.google.protobuf", version = "0.9.4" }
-
-download = { id = "de.undercouch.download", version = "5.6.0" }
+download = { id = "de.undercouch.download", version = "5.7.0" }
 baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }
 baselineprofile = { id = "androidx.baselineprofile", version.ref = "baselineprofile" }