From e353fa2b819a620dddad2e2fd95e5164652bde03 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sat, 23 May 2026 03:35:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 3 +- .../app/ui/main/explore/ExploreScreen.kt | 4 +- .../homepage/HomepageModuleManageSheet.kt | 52 +++++++++++++------ .../app/ui/main/homepage/HomepageScreen.kt | 1 + .../app/ui/main/homepage/HomepageViewModel.kt | 14 ++--- .../io/legado/app/ui/main/rss/RssScreen.kt | 4 +- .../ui/widget/components/LoadMoreFooter.kt | 8 +-- .../ui/widget/components/list/ListScaffold.kt | 6 ++- 8 files changed, 60 insertions(+), 32 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 279d0ab39..2fb5ef806 100644 --- a/.claude/settings.local.json +++ b/.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 *)" ] } } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt index ae9eea454..63848142d 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt +++ b/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()) { diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt index 4f90b9e9d..8a19c31bb 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt @@ -87,6 +87,7 @@ fun HomepageModuleManageSheet( onGetAllModulesGroupedBySource: () -> Map> = { emptyMap() }, onGetSourceName: (String) -> String = { it }, onAssignModuleToCustomSet: (String, String?) -> Unit = { _, _ -> }, + onSyncSourceModules: (String) -> Unit = {}, ) { var selectingSetUrl by remember(data != null) { mutableStateOf(null) } var browsingSourceUrl by remember(data != null) { mutableStateOf(null) } @@ -208,18 +209,27 @@ fun 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 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 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 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 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 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 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 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 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 -> diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt index 52bfaaa27..49ad107f4 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt +++ b/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( diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt index f3f618cec..726e3238a 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt +++ b/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(extraBufferCapacity = 8) val effects = _effects.asSharedFlow() - private val loadJobs = mutableMapOf() + private val loadJobs = ConcurrentHashMap() 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 { val source = resolveBookSource(sourceUrl) ?: return emptyList() - // 按需同步:只有进入该源的管理页才同步其 JSON 定义 - viewModelScope.launch { - syncModulesFromSource(source) - } - val json = source.homepageModules ?: return emptyList() val jsonDefs = parseBookSourceModules(source, json) diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt index 5ae05fdfb..a46da6b17 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt +++ b/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), diff --git a/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt b/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt index e457690a7..d1950c69a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt +++ b/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 = "尝试加载更多" - ) } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt index 3fb74f0d9..e2421ce43 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt +++ b/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 ListScaffold( } }, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit ) { val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() @@ -102,7 +105,8 @@ fun ListScaffold( bottomContent = bottomContent ) }, - floatingActionButton = floatingActionButton + floatingActionButton = floatingActionButton, + contentWindowInsets = contentWindowInsets ) { paddingValues -> Box( modifier = Modifier.fillMaxSize()