HapeLee 5 months ago
parent
commit
e353fa2b81

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

@@ -101,7 +101,8 @@
       "Read(//c/Users/**)",
       "Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
       "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 *)"
     ]
   }
 }

+ 3 - 1
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
@@ -151,7 +152,8 @@ fun ExploreScreen(
                     onClick = { viewModel.setGroup(group); dismiss() }
                 )
             }
-        }
+        },
+        contentWindowInsets = WindowInsets(0)
     ) { paddingValues ->
         Box(modifier = Modifier.fillMaxSize()) {
             if (uiState.items.isEmpty()) {

+ 37 - 15
app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt

@@ -87,6 +87,7 @@ fun <T> HomepageModuleManageSheet(
     onGetAllModulesGroupedBySource: () -> Map<String, List<HomepageModuleManageUi>> = { emptyMap() },
     onGetSourceName: (String) -> String = { it },
     onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> },
+    onSyncSourceModules: (String) -> Unit = {},
 ) {
     var selectingSetUrl by remember(data != null) { mutableStateOf<String?>(null) }
     var browsingSourceUrl by remember(data != null) { mutableStateOf<String?>(null) }
@@ -208,18 +209,27 @@ fun <T> HomepageModuleManageSheet(
         val setUrl = selectingSetUrl
         val browseUrl = browsingSourceUrl
         val isBrowsing = showSourceBrowser || browseUrl != null
+
+        LaunchedEffect(browseUrl) {
+            browseUrl?.let { onSyncSourceModules(it) }
+        }
+
         when {
             browseUrl != null && browsingDetail -> {
                 // 三级:浏览书源的模块列表(已加入 / 书源模块 / 发现)
                 val displaySetUrl =
                     selectingSetUrl ?: HomepageViewModel.customSetUrl("src_$browseUrl")
                 val currentSetId = HomepageViewModel.customSetIdFromUrl(displaySetUrl)
-                val joinedModules = onGetModulesInSet(displaySetUrl)
+                val joinedModules = remember(displaySetUrl, sets, browseSources) {
+                    onGetModulesInSet(displaySetUrl).distinctBy { it.id }
+                }
 
-                val standardModules =
+                val standardModules = remember(joinedModules) {
                     joinedModules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
-                val infiniteModules =
+                }
+                val infiniteModules = remember(joinedModules) {
                     joinedModules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+                }
 
                 val joinedKeys = joinedModules.map { it.moduleKey }.toSet()
                 val sourceModules = onGetSourceModules(browseUrl, currentSetId)
@@ -247,9 +257,9 @@ fun <T> HomepageModuleManageSheet(
                                     AppText(stringResource(R.string.homepage_no_joined_modules))
                                 }
                             } else {
-                                var listData by remember(displaySetUrl) {
+                                var listData by remember(displaySetUrl, standardModules) {
                                     mutableStateOf(
-                                        standardModules
+                                        standardModules.distinctBy { it.id }
                                     )
                                 }
                                 val listState = rememberLazyListState()
@@ -268,7 +278,7 @@ fun <T> HomepageModuleManageSheet(
                                 LaunchedEffect(reorderableState.isAnyItemDragging) {
                                     if (!reorderableState.isAnyItemDragging) {
                                         val orderedIds =
-                                            listData.map { it.id } + infiniteModules.map { it.id }
+                                            (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
                                         if (orderedIds != joinedModules.map { it.id }) {
                                             onReorderModules(orderedIds)
                                         }
@@ -513,7 +523,7 @@ fun <T> HomepageModuleManageSheet(
                                 modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
                             )
                         }
-                        items(modules, key = { it.sourceUrl + it.moduleKey }) { module ->
+                        items(modules, key = { it.id }) { module ->
                             val instanceIdInCurrentSet = joinedInCurrent[module.moduleKey]
                             val inCurrentSet = instanceIdInCurrentSet != null
                             SelectionItemCard(
@@ -540,11 +550,14 @@ fun <T> HomepageModuleManageSheet(
 
             isBrowsing -> {
                 // 二级:浏览书源列表
+                val sources = remember(filteredBrowseSources) {
+                    filteredBrowseSources.distinctBy { it.sourceUrl }
+                }
                 LazyColumn(
                     modifier = Modifier.fillMaxWidth(),
                     verticalArrangement = Arrangement.spacedBy(8.dp)
                 ) {
-                    items(filteredBrowseSources, key = { it.sourceUrl }) { source ->
+                    items(sources, key = { it.sourceUrl }) { source ->
                         val moduleCount = onGetSourceModules(source.sourceUrl, null).size
                         SelectionItemCard(
                             title = source.sourceName,
@@ -562,12 +575,16 @@ fun <T> HomepageModuleManageSheet(
             setUrl != null && HomepageViewModel.isCustomSetUrl(setUrl) -> {
                 // 二级:集详情
                 val setId = HomepageViewModel.customSetIdFromUrl(setUrl)
-                val modules = onGetModulesInSet(setUrl)
+                val modules = remember(setUrl, sets) {
+                    onGetModulesInSet(setUrl).distinctBy { it.id }
+                }
 
-                val standardModules =
+                val standardModules = remember(modules) {
                     modules.filter { !HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
-                val infiniteModules =
+                }
+                val infiniteModules = remember(modules) {
                     modules.filter { HomepageViewModel.isInfinite(it.type, it.layoutConfig) }
+                }
 
                 if (modules.isEmpty()) {
                     Column(
@@ -590,7 +607,9 @@ fun <T> HomepageModuleManageSheet(
                         )
                     }
                 } else {
-                    var listData by remember(setUrl) { mutableStateOf(standardModules) }
+                    var listData by remember(setUrl, standardModules) {
+                        mutableStateOf(standardModules)
+                    }
                     val listState = rememberLazyListState()
                     val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
                         listData = listData.toMutableList().apply {
@@ -602,11 +621,12 @@ fun <T> HomepageModuleManageSheet(
 
                     LaunchedEffect(standardModules) {
                         if (!reorderableState.isAnyItemDragging) listData =
-                            standardModules.distinctBy { it.id }
+                            standardModules
                     }
                     LaunchedEffect(reorderableState.isAnyItemDragging) {
                         if (!reorderableState.isAnyItemDragging) {
-                            val orderedIds = listData.map { it.id } + infiniteModules.map { it.id }
+                            val orderedIds =
+                                (listData.map { it.id } + infiniteModules.map { it.id }).distinct()
                             if (orderedIds != modules.map { it.id }) onReorderModules(orderedIds)
                         }
                     }
@@ -703,7 +723,9 @@ fun <T> HomepageModuleManageSheet(
 
             else -> {
                 // 一级:集列表
-                var localSets by remember(data != null) { mutableStateOf(sets) }
+                var localSets by remember(data != null, sets) {
+                    mutableStateOf(sets.distinctBy { it.sourceUrl })
+                }
                 val setsListState = rememberLazyListState()
                 val setsReorderableState =
                     rememberReorderableLazyListState(setsListState) { from, to ->

+ 1 - 0
app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt

@@ -266,6 +266,7 @@ fun HomepageScreen(
             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(

+ 8 - 6
app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt

@@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import java.security.MessageDigest
+import java.util.concurrent.ConcurrentHashMap
 
 class HomepageViewModel(
     application: Application,
@@ -80,7 +81,7 @@ class HomepageViewModel(
     private val _effects = MutableSharedFlow<HomepageEffect>(extraBufferCapacity = 8)
     val effects = _effects.asSharedFlow()
 
-    private val loadJobs = mutableMapOf<String, Job>()
+    private val loadJobs = ConcurrentHashMap<String, Job>()
     private val initModulesSyncFlow = bookSourceRepository.flowHomepageModules()
     private val exploreSourcesFlow = bookSourceRepository.flowExploreSources()
 
@@ -688,6 +689,12 @@ class HomepageViewModel(
         }
     }
 
+    fun syncSourceModules(sourceUrl: String) {
+        viewModelScope.launch {
+            resolveBookSource(sourceUrl)?.let { syncModulesFromSource(it) }
+        }
+    }
+
     /** 「书源模块」tab:仅 JSON,纯参考 */
     fun getSourceModules(
         sourceUrl: String,
@@ -695,11 +702,6 @@ class HomepageViewModel(
     ): List<HomepageModuleManageUi> {
         val source = resolveBookSource(sourceUrl) ?: return emptyList()
 
-        // 按需同步:只有进入该源的管理页才同步其 JSON 定义
-        viewModelScope.launch {
-            syncModulesFromSource(source)
-        }
-
         val json = source.homepageModules ?: return emptyList()
         val jsonDefs = parseBookSourceModules(source, json)
 

+ 3 - 1
app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
@@ -165,7 +166,8 @@ fun RssScreen(
                     }
                 )
             }
-        }
+        },
+        contentWindowInsets = WindowInsets(0)
     ) { paddingValues ->
         LazyVerticalGrid(
             columns = GridCells.Adaptive(minSize = 72.dp),

+ 1 - 7
app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt

@@ -26,7 +26,6 @@ import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.button.AnimatedTextButton
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
@@ -157,7 +156,7 @@ fun LoadMoreFooter(
                         AppContainedLoadingIndicator()
 
                         AppText(
-                            text = "正在加载更多内容…",
+                            text = "正在加载…",
                             color = LegadoTheme.colorScheme.outline,
                             style = LegadoTheme.typography.bodySmall
                         )
@@ -239,11 +238,6 @@ fun LoadMoreFooter(
                             }
                         }
                     }
-                    AnimatedTextButton(
-                        isLoading = false,
-                        onClick = onRetry,
-                        text = "尝试加载更多"
-                    )
                 }
             }
         }

+ 5 - 1
app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt

@@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
@@ -16,6 +17,7 @@ import androidx.compose.material.icons.filled.Add
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
+import androidx.compose.material3.ScaffoldDefaults
 import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.animateFloatingActionButton
@@ -69,6 +71,7 @@ fun <T> ListScaffold(
         }
     },
     snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
+    contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     content: @Composable (PaddingValues) -> Unit
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
@@ -102,7 +105,8 @@ fun <T> ListScaffold(
                 bottomContent = bottomContent
             )
         },
-        floatingActionButton = floatingActionButton
+        floatingActionButton = floatingActionButton,
+        contentWindowInsets = contentWindowInsets
     ) { paddingValues ->
         Box(
             modifier = Modifier.fillMaxSize()