diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 017c01579..279d0ab39 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -101,8 +101,7 @@ "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\\\\\", \"\"\\) })", - "Bash(powershell *)" + "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\\\\\", \"\"\\) })" ] } } diff --git a/app/src/main/assets/web/help/md/homepageModulesHelp.md b/app/src/main/assets/web/help/md/homepageModulesHelp.md index 0d4d4aa54..76d6347e5 100644 --- a/app/src/main/assets/web/help/md/homepageModulesHelp.md +++ b/app/src/main/assets/web/help/md/homepageModulesHelp.md @@ -1,240 +1,115 @@ -# 首页模块 (homepageModules) +# 首页模块 (Homepage Modules) 配置规范 -在书源编辑页的「发现」Tab 中,可以配置 `首页模块 (JSON)` 字段,声明该书源为首页提供哪些内容模块。 +书源的 `homepageModules` 字段允许开发者声明该书源在首页展示的内容模块。这些模块通过 JSON +数组进行定义,支持高度自定义的布局和数据来源。 -## 概述 +--- -- 每个模块代表首页上的一个内容区块(如轮播图、排行榜、网格书架等) -- 一个书源可以声明多个模块(例如同时提供"热门推荐"Banner 和"周榜"排行榜) -- 用户在首页可以自由拖拽排序、隐藏/显示各个模块 -- 模块内容来自书源的**发现(explore)**接口,通过 `kindTitle` 匹配分类URL +## 1. 数据结构 (Data Structure) -## JSON 格式 +`homepageModules` 是一个包含多个模块定义对象的 JSON 数组。 -`homepageModules` 是一个 JSON 数组,每个元素定义一个模块: +### 模块通用字段 + +| 字段 | 类型 | 必须 | 说明 | +|:-----------------|:---------|:---|:------------------------------------------------------| +| **key** | `String` | 是 | 模块唯一标识。建议使用 `[a-z0-9_]` 字符。用于保存用户的排序/显隐设置。 | +| **type** | `Enum` | 是 | 模块类型。定义了渲染方式和交互逻辑。详见 [模块类型](#2-模块类型-module-types)。 | +| **title** | `String` | 是 | 模块默认标题。用户可在本地自定义覆盖。 | +| **kindTitle** | `String` | 否 | 用于匹配书源「发现」规则中的分类标题。匹配成功后自动继承其 URL 和规则。 | +| **url** | `String` | 否 | 显式指定数据接口 URL。优先级高于 `kindTitle`。支持变量替换。 | +| **args** | `String` | 否 | 附加参数。在 `buttonGroup` 类型中为 JSON 数组字符串。 | +| **layoutConfig** | `Object` | 否 | 布局配置对象,用于调整列数、行数、图标等。详见 [布局配置](#3-布局配置-layoutconfig)。 | + +--- + +## 2. 模块类型 (Module Types) + +### 列表与轮播类 + +| 类型 (Type) | 描述 | 特点 | +|:----------|:------|:-----------------------| +| `banner` | 横滑轮播图 | 适合展示高权重的精品推荐,使用大图封面。 | +| `ranking` | 排行榜列表 | 垂直列表展示,带排名序号。 | +| `card` | 推荐卡片 | 横向滑动的卡片流,同时显示封面、标题及简介。 | + +### 网格类 + +| 类型 (Type) | 描述 | 特点 | +|:---------------|:------|:------------------| +| `grid` | 标准网格 | 最常用的展示形式。支持自定义行列。 | +| `gridRanking` | 网格排行榜 | 多行多列的排行展示。横向翻页。 | +| `infiniteGrid` | 无限网格 | 垂直滚动的网格流。无限加载。 | +| `waterfall` | 错位瀑布流 | 垂直错位排列的书架流。无限加载。 | + +### 功能类 + +| 类型 (Type) | 描述 | 特点 | +|:--------------|:------|:--------------------------------------------| +| `buttonGroup` | 快捷按钮组 | 渲染为一组圆形/图标按钮,支持自动填充宽度与自动分列。通常用于放置常用分类或功能入口。 | + +--- + +## 3. 布局配置 (LayoutConfig) + +通过 `layoutConfig` 对象,可以精细化控制模块的表现。 + +| 属性 (Property) | 类型 | 适用类型 | 默认值 | 说明 | +|:--------------|:---------|:------------------------------------|:----|:-----------------------------------------| +| `columns` | `Int` | `grid`, `waterfall`, `infiniteGrid` | 3 | 每行显示的列数。 | +| `icon` | `String` | `buttonGroup` | - | 按钮组的默认统一图标 URL。 | +| `icons` | `Object` | `buttonGroup` | - | 图标映射表。例:`{"排行": "http://path/to/icon"}`。 | + +--- + +## 4. 数据绑定逻辑 (Data Binding) + +1. **自动匹配**:如果提供了 `kindTitle`,系统会遍历书源 `exploreKinds()` 返回的列表。如果某个分类的 + `title` 与之完全一致,该模块将自动使用该分类的 `url`。 +2. **静态指定**:如果提供了 `url`,系统将直接请求该 URL。 +3. **降级逻辑**:若 `kindTitle` 未匹配且无 `url`,模块将回退至书源的主 `exploreUrl`。 + +--- + +## 5. 完整 JSON 示例 ```json [ { - "key": "模块唯一标识", - "type": "模块类型", - "title": "模块标题", - "kindTitle": "匹配的分类标题(可选)", - "url": "覆盖分类URL(可选)", - "args": "特殊参数(可选)", - "layoutConfig": { - "columns": 2, - "rows": 3 - } - } -] -``` - -### 字段说明 - -| 字段 | 类型 | 必填 | 说明 | -|----------------|-----------|----|-------------------------------------------------------------------------------------------------------| -| `key` | `String` | 是 | 模块在书源内的唯一标识,用于关联用户偏好。建议使用英文,如 `"hot"`, `"rank_week"` | -| `type` | `String` | 是 | 模块类型,可选值:`"banner"`, `"ranking"`, `"gridRanking"`, `"grid"`, `"card"`, `"waterfall"`, `"buttonGroup"` | -| `title` | `String` | 是 | 模块标题,会在首页模块头部展示 | -| `kindTitle` | `String?` | 否 | 用于匹配该书源「发现」中的分类标题。不填则使用默认 `exploreUrl` | -| `url` | `String?` | 否 | 显式指定该模块的 URL,优先级高于 `kindTitle` 匹配到的 URL | -| `args` | `String?` | 否 | 模块特有参数。在 `buttonGroup` 中为包含分类标题的 JSON 数组字符串 | -| `layoutConfig` | `Object?` | 否 | 布局配置对象。支持 `columns` (列数), `rows` (行数) | - -## 模块类型 - -### banner — 横滑轮播图 - -适合展示热门推荐、本周强推等内容。以大图封面横向滑动展示。 - -```json -{ - "key": "hot_banner", - "type": "banner", - "title": "热门推荐", - "kindTitle": "热门", - "displayCount": 6 -} -``` - -### ranking — 排行榜 - -带排名序号的列表。前三名高亮为橙色,其余为灰色。默认折叠显示前 5 本,末尾有"展开更多"按钮。 - -```json -{ - "key": "week_rank", - "type": "ranking", - "title": "周排行榜", - "kindTitle": "周榜", - "displayCount": 10 -} -``` - -### gridRanking — 网格排行 - -4×4 列式排列(可通过 `layoutConfig.rows` 修改行数),每页多本书,可横向翻页。封面较小,仅显示书名和作者。适合作为首页入口展示大量书籍。 - -```json -{ - "key": "all_grid", - "type": "gridRanking", - "title": "全部分类", - "kindTitle": "全部分类", - "layoutConfig": { - "rows": 4 - } -} -``` - -### grid — 网格书架 - -网格布局,适合展示分类书单。支持自定义行列。 - -- `columns`: 默认 3 -- `rows`: 默认 2。若设置为 `0`,则显示为平铺列表并支持下拉加载更多。 - -```json -{ - "key": "scifi_grid", - "type": "grid", - "title": "科幻精选", - "kindTitle": "科幻", - "layoutConfig": { - "columns": 3, - "rows": 2 - } -} -``` - -### card — 推荐卡片 - -大图横向滑动卡片,展示封面 + 书名 + 简介。适合需要更多信息展示的场景。 - -```json -{ - "key": "editor_pick", - "type": "card", - "title": "编辑推荐", - "kindTitle": "编辑推荐" -} -``` - -### waterfall — 错位瀑布流 - -瀑布流布局,展示封面 + 书名 + 简介。支持 `layoutConfig.columns` 自定义列数(默认 2)。支持无限加载更多。 - -```json -{ - "key": "hot_wf", - "type": "waterfall", - "title": "大家都在看", - "kindTitle": "热门", - "layoutConfig": { - "columns": 2 - } -} -``` - -### buttonGroup — 按钮组 - -显示为网格排列的分类按钮。适合放置常用的分类或动作入口。 - -- **布局特性**:自动平衡每行按钮数量(例如 6 个按钮显示为 3+3,8 个显示为 4+4)。每行最多 5 个。 -- **args**: 可选。由分类标题组成的 JSON 数组字符串,如 `["排行", "分类", "完本"]`。若不填则默认显示该书源前 - 5 个分类。 -- **layoutConfig**: - - `icon`: 全局默认图标。支持网络图片 URL。 - - `icons`: 图标映射对象。以分类标题为键,网络图片 URL 为值。 - -```json -{ - "key": "entry_buttons", - "type": "buttonGroup", - "title": "快捷入口", - "args": "[\"排行\", \"分类\", \"我的\"]", - "layoutConfig": { - "icon": "https://example.com/default.png", - "icons": { - "排行": "https://example.com/rank.png", - "我的": "https://example.com/my.png" - } - } -} -``` - -## kindTitle 匹配规则 - -模块加载时,系统会根据 `kindTitle` 在书源的「发现」分类列表中进行匹配: - -1. 在 `exploreKinds()`(即发现页的分类列表)中查找 `title` 等于 `kindTitle` 的分类 -2. 如果匹配成功,使用该分类的 `url` 来加载数据 -3. 如果匹配失败,或 `kindTitle` 为空,则使用书源的默认 `exploreUrl` -4. 如果两个 URL 都没有,该模块将显示加载错误 - -**建议**:确保 `kindTitle` 的值与发现页分类的标题**完全一致**(包括大小写和标点)。 - -## 完整示例 - -以下是一个书源配置了多种模块的完整 JSON: - -```json -[ - { - "key": "hot", + "key": "top_banner", "type": "banner", - "title": "热门推荐", - "kindTitle": "热门", - "displayCount": 6 + "title": "精品强推", + "kindTitle": "首页推荐" }, { - "key": "rank_week", + "key": "quick_nav", + "type": "buttonGroup", + "title": "分类导航", + "args": "[\"武侠\", \"仙侠\", \"都市\", \"历史\"]", + "layoutConfig": { + "icon": "https://example.com/icons/default.png", + "icons": { + "武侠": "https://example.com/icons/wuxia.png" + } + } + }, + { + "key": "hot_rank", "type": "ranking", - "title": "周榜", - "kindTitle": "周排行", - "displayCount": 10 + "title": "热门榜单", + "kindTitle": "排行榜", + "layoutConfig": { + "rows": 5 + } }, { - "key": "rank_month", - "type": "ranking", - "title": "月榜", - "kindTitle": "月排行", - "displayCount": 10 - }, - { - "key": "scifi", - "type": "grid", - "title": "科幻精选", - "kindTitle": "科幻", - "displayCount": 6 - }, - { - "key": "new_book", - "type": "card", - "title": "新书上架", - "kindTitle": "新书", - "displayCount": 8 + "key": "explore_waterfall", + "type": "waterfall", + "title": "发现更多", + "kindTitle": "全部", + "layoutConfig": { + "columns": 2 + } } ] ``` - -每个 `key` 必须唯一,系统会按 JSON 数组中的声明顺序创建模块标识。 - -## 用户体验 - -- **发现模块**:首页会自动出现配置了 `homepageModules` 的已启用书源所声明的模块 -- **拖拽排序**:长按模块标题旁的拖拽手柄可调整顺序 -- **隐藏模块**:编辑模式下可隐藏不需要的模块 -- **下拉刷新**:刷新后所有模块重新加载数据 -- **点击书籍**:跳转到书籍详情页 -- **点击模块标题**:跳转到该书源的完整发现页 - -## 注意事项 - -- JSON 格式必须**严格有效**,建议使用在线 JSON 校验工具检查 -- 如果 JSON 解析失败,该书源的所有模块将被静默跳过 -- 模块使用的图片从书籍封面(`coverUrl`)获取,请确保发现规则正确提取了封面 -- `homepageModules` 为空的旧书源不会报错,也不会有首页模块展示 -- 同一书源可配置多个同类型模块(如多个排行榜),只需 `key` 不同即可 -- 模块标识全局唯一 ID 格式为 `"{setId}::{书源URL}::{key}"`,其中 `setId` 用于区分模块所属的集(书源集或自定义集)。 diff --git a/app/src/main/java/io/legado/app/data/dao/HomepageCustomSetDao.kt b/app/src/main/java/io/legado/app/data/dao/HomepageCustomSetDao.kt index c127084b5..5eee9645d 100644 --- a/app/src/main/java/io/legado/app/data/dao/HomepageCustomSetDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/HomepageCustomSetDao.kt @@ -25,6 +25,11 @@ interface HomepageCustomSetDao { @Query("UPDATE homepage_custom_sets SET sortOrder = :order WHERE id = :id") suspend fun setSortOrder(id: String, order: Int) + @androidx.room.Transaction + suspend fun batchSetSortOrders(orders: Map) { + orders.forEach { (id, order) -> setSortOrder(id, order) } + } + @Query("DELETE FROM homepage_custom_sets WHERE id = :id") suspend fun delete(id: String) } diff --git a/app/src/main/java/io/legado/app/data/dao/HomepageModuleDao.kt b/app/src/main/java/io/legado/app/data/dao/HomepageModuleDao.kt index a35af7897..3e4f84132 100644 --- a/app/src/main/java/io/legado/app/data/dao/HomepageModuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/HomepageModuleDao.kt @@ -28,6 +28,11 @@ interface HomepageModuleDao { @Query("UPDATE homepage_modules SET sortOrder = :order WHERE id = :id") suspend fun setSortOrder(id: String, order: Int) + @androidx.room.Transaction + suspend fun batchSetSortOrders(orders: Map) { + orders.forEach { (id, order) -> setSortOrder(id, order) } + } + @Query("UPDATE homepage_modules SET customSetTitle = :title WHERE id = :id") suspend fun setCustomSetTitle(id: String, title: String?) diff --git a/app/src/main/java/io/legado/app/data/repository/HomepageModulesRepository.kt b/app/src/main/java/io/legado/app/data/repository/HomepageModulesRepository.kt index 4f7c0e905..555f5e992 100644 --- a/app/src/main/java/io/legado/app/data/repository/HomepageModulesRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/HomepageModulesRepository.kt @@ -34,6 +34,8 @@ class HomepageModulesRepository( moduleDao.setEnabled(id, enabled) override suspend fun setSortOrder(id: String, order: Int) = moduleDao.setSortOrder(id, order) + override suspend fun batchSetSortOrders(orders: Map) = + moduleDao.batchSetSortOrders(orders) override suspend fun setCustomSetId(id: String, setId: String?) = moduleDao.setCustomSetId(id, setId) @@ -55,6 +57,8 @@ class HomepageModulesRepository( override suspend fun setCustomSetSortOrder(id: String, order: Int) = customSetDao.setSortOrder(id, order) + override suspend fun batchSetCustomSetSortOrders(orders: Map) = + customSetDao.batchSetSortOrders(orders) override suspend fun createCustomSet(name: String): CustomSetItem { val entity = HomepageCustomSet( diff --git a/app/src/main/java/io/legado/app/domain/gateway/HomepageModulesGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/HomepageModulesGateway.kt index 0cdd8baa4..7b03db6f2 100644 --- a/app/src/main/java/io/legado/app/domain/gateway/HomepageModulesGateway.kt +++ b/app/src/main/java/io/legado/app/domain/gateway/HomepageModulesGateway.kt @@ -15,6 +15,7 @@ interface HomepageModulesGateway { suspend fun upsertAll(modules: List) suspend fun setEnabled(id: String, enabled: Boolean) suspend fun setSortOrder(id: String, order: Int) + suspend fun batchSetSortOrders(orders: Map) suspend fun setCustomSetId(id: String, setId: String?) suspend fun setCustomSetTitle(id: String, title: String?) suspend fun delete(id: String) @@ -27,6 +28,7 @@ interface HomepageModulesGateway { // Custom set mutations suspend fun upsertCustomSet(set: CustomSetItem) suspend fun setCustomSetSortOrder(id: String, order: Int) + suspend fun batchSetCustomSetSortOrders(orders: Map) suspend fun createCustomSet(name: String): CustomSetItem suspend fun renameCustomSet(id: String, name: String) suspend fun deleteCustomSet(id: String) diff --git a/app/src/main/java/io/legado/app/domain/usecase/AddBookUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/AddBookUseCase.kt index d6af61224..4c2e59ed8 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/AddBookUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/AddBookUseCase.kt @@ -33,8 +33,9 @@ class AddBookUseCase( if (source == null) { for (bookSourcePart in hasBookUrlPattern) { try { - val bs = bookSourcePart.getBookSource()!! - if (bookUrl.matches(bs.bookUrlPattern!!.toRegex())) { + val bs = bookSourcePart.getBookSource() ?: continue + val pattern = bs.bookUrlPattern ?: continue + if (bookUrl.matches(pattern.toRegex())) { source = bs break } @@ -50,17 +51,16 @@ class AddBookUseCase( ) kotlin.runCatching { - WebBook.getBookInfoAwait(bookSource, book) - }.onSuccess { - val dbBook = bookRepository.getBook(it.name, it.author) + val bookInfo = WebBook.getBookInfoAwait(bookSource, book) + val dbBook = bookRepository.getBook(bookInfo.name, bookInfo.author) if (dbBook != null) { - val toc = WebBook.getChapterListAwait(bookSource, it).getOrThrow() - dbBook.migrateTo(it, toc) - bookRepository.insert(it) + val toc = WebBook.getChapterListAwait(bookSource, bookInfo).getOrThrow() + dbBook.migrateTo(bookInfo, toc) + bookRepository.insert(bookInfo) bookRepository.insertChapters(*toc.toTypedArray()) } else { - it.order = bookRepository.getMinOrder() - 1 - bookRepository.insert(it) + bookInfo.order = bookRepository.getMinOrder() - 1 + bookRepository.insert(bookInfo) } successCount++ onProgress(successCount) diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageLayoutSheet.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageLayoutSheet.kt index d25dab285..79dbe6104 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageLayoutSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageLayoutSheet.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable 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.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem @@ -34,13 +36,16 @@ fun HomepageLayoutSheet( AppModalBottomSheet( data = data, onDismissRequest = onDismissRequest, - title = "布局设置", + title = stringResource(R.string.homepage_layout_settings), ) { Column { DropdownListSettingItem( - title = "首页布局模式", + title = stringResource(R.string.homepage_layout_mode), selectedValue = layoutMode.toString(), - displayEntries = arrayOf("混合列表", "分源Tab"), + displayEntries = arrayOf( + stringResource(R.string.homepage_layout_mixed), + stringResource(R.string.homepage_layout_tabs) + ), entryValues = arrayOf("0", "1"), onValueChange = { onLayoutModeChange(it.toInt()) } ) 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 ceb0bb2ab..ac998e93a 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 @@ -21,8 +21,6 @@ import androidx.compose.material.icons.filled.DriveFileRenameOutline import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FilterList import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,14 +30,15 @@ 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.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.JsonKeyEditorConfig import io.legado.app.ui.widget.components.JsonRawEditor import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.SecondaryButton @@ -104,7 +103,8 @@ fun HomepageModuleManageSheet( var selectedKindTitles by remember(data != null) { mutableStateOf>(emptySet()) } var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) } var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) } - var tempButtonGroupTitle by remember(data != null) { mutableStateOf("快捷操作") } + val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions) + var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) } val currentTargetSetId = remember(selectingSetUrl) { selectingSetUrl?.let { HomepageViewModel.customSetIdFromUrl(it) } @@ -130,15 +130,17 @@ fun HomepageModuleManageSheet( groupFilter = null }, title = when { - showCustomSetAddModules -> "添加模块" + showCustomSetAddModules -> stringResource(R.string.homepage_add_module) browsingSourceUrl != null && browsingDetail -> - browseSources.find { it.sourceUrl == browsingSourceUrl }?.sourceName ?: "模块列表" + browseSources.find { it.sourceUrl == browsingSourceUrl }?.sourceName + ?: stringResource(R.string.homepage_module_list) - showSourceBrowser || browsingSourceUrl != null -> "浏览书源模块" + showSourceBrowser || browsingSourceUrl != null -> stringResource(R.string.homepage_browse_source_modules) selectingSetUrl != null && HomepageViewModel.isCustomSetUrl(selectingSetUrl!!) -> - (sets.find { it.sourceUrl == selectingSetUrl }?.sourceName ?: "集详情") + (sets.find { it.sourceUrl == selectingSetUrl }?.sourceName + ?: stringResource(R.string.homepage_set_detail)) - else -> "首页模块管理" + else -> stringResource(R.string.homepage_module_manage) }, startAction = { if (showCustomSetAddModules) { @@ -179,7 +181,7 @@ fun HomepageModuleManageSheet( expanded = expanded, onDismissRequest = { expanded = false }) { RoundDropdownMenuItem( - text = "全部分组", + text = stringResource(R.string.homepage_all_groups), onClick = { groupFilter = null; expanded = false }, trailingIcon = if (groupFilter == null) { { AppIcon(Icons.Default.Check, null, Modifier.size(18.dp)) } @@ -221,7 +223,11 @@ fun HomepageModuleManageSheet( Column { AppTabRow( - tabTitles = listOf("已加入", "书源模块", "发现"), + 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 } ) @@ -234,7 +240,7 @@ fun HomepageModuleManageSheet( .padding(24.dp), contentAlignment = Alignment.Center ) { - AppText("暂无已加入的模块") + AppText(stringResource(R.string.homepage_no_joined_modules)) } } else { var listData by remember(displaySetUrl) { @@ -271,9 +277,9 @@ fun HomepageModuleManageSheet( ) { item(key = "header_standard") { AppText( - text = "标准模块 (可拖拽排序)", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, + 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 @@ -318,9 +324,8 @@ fun HomepageModuleManageSheet( ) ) AppText( - text = "底栏无限模块", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.secondary, + text = stringResource(R.string.homepage_infinite_module_bottom), + style = LegadoTheme.typography.labelMedium, modifier = Modifier.padding( horizontal = 16.dp, vertical = 4.dp @@ -333,7 +338,9 @@ fun HomepageModuleManageSheet( infiniteModules.firstOrNull() == module SelectionItemCard( title = module.title, - subtitle = HomepageModuleType.fromKey(module.type).title + if (isEffective) " · 当前生效" else " · 已被屏蔽", + 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) }, @@ -363,7 +370,7 @@ fun HomepageModuleManageSheet( .padding(24.dp), contentAlignment = Alignment.Center ) { - AppText("该书源的 homepageModules JSON 为空") + AppText(stringResource(R.string.homepage_source_json_empty)) } } else { LazyColumn( @@ -376,7 +383,9 @@ fun HomepageModuleManageSheet( val isJoined = joinedKeys.contains(module.moduleKey) SelectionItemCard( title = module.title, - subtitle = module.moduleKey + if (isJoined) " · 已加入" else "", + subtitle = module.moduleKey + if (isJoined) stringResource( + R.string.homepage_status_joined + ) else "", containerColor = LegadoTheme.colorScheme.onSheetContent, isSelected = isJoined, inSelectionMode = true, @@ -405,7 +414,7 @@ fun HomepageModuleManageSheet( HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } } CompactDropdownSettingItem( - title = "模块类型", + title = stringResource(R.string.homepage_module_type), selectedValue = browseModuleType, displayEntries = typeList.map { it.title }.toTypedArray(), entryValues = typeList.map { it.key }.toTypedArray(), @@ -421,14 +430,14 @@ fun HomepageModuleManageSheet( contentAlignment = Alignment.Center ) { AppText( - "该书源暂无发现项", - color = MaterialTheme.colorScheme.onSurfaceVariant + stringResource(R.string.homepage_source_no_discover), + color = LegadoTheme.colorScheme.onSurfaceVariant ) } } else { AppText( - "选择项", - style = MaterialTheme.typography.labelMedium, + stringResource(R.string.homepage_select_items), + style = LegadoTheme.typography.labelMedium, modifier = Modifier.padding( horizontal = 16.dp, vertical = 4.dp @@ -462,7 +471,9 @@ fun HomepageModuleManageSheet( val isJoined = joinedKeys.contains(kindTitle) SelectionItemCard( title = kindTitle, - subtitle = kindUrl.take(60) + if (isJoined) " · 已加入" else "", + subtitle = kindUrl.take(60) + if (isJoined) stringResource( + R.string.homepage_status_joined + ) else "", containerColor = LegadoTheme.colorScheme.onSheetContent, isSelected = isJoined, inSelectionMode = true, @@ -482,7 +493,7 @@ fun HomepageModuleManageSheet( } Spacer(modifier = Modifier.height(12.dp)) SecondaryButton( - text = "+ 手动添加", + text = stringResource(R.string.homepage_manual_add), onClick = { addDialogPrefill = AddDialogPrefill(type = browseModuleType) }, @@ -511,7 +522,7 @@ fun HomepageModuleManageSheet( item(key = "header_$sourceUrl") { AppText( text = onGetSourceName(sourceUrl), - style = MaterialTheme.typography.labelLarge, + style = LegadoTheme.typography.labelLarge, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) } @@ -551,7 +562,7 @@ fun HomepageModuleManageSheet( val moduleCount = onGetSourceModules(source.sourceUrl, null).size SelectionItemCard( title = source.sourceName, - subtitle = "$moduleCount 个模块", + subtitle = stringResource(R.string.homepage_n_modules, moduleCount), containerColor = LegadoTheme.colorScheme.onSheetContent, onToggleSelection = { browsingSourceUrl = source.sourceUrl @@ -580,9 +591,9 @@ fun HomepageModuleManageSheet( .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - AppText("暂无模块") + AppText(stringResource(R.string.homepage_no_modules)) SecondaryButton( - text = "浏览书源模块添加", + text = stringResource(R.string.homepage_browse_to_add), onClick = { if (setId.startsWith("src_")) { browsingSourceUrl = setId.removePrefix("src_") @@ -623,8 +634,8 @@ fun HomepageModuleManageSheet( if (listData.isNotEmpty()) { item(key = "header_std_detail") { AppText( - text = "标准模块", - style = MaterialTheme.typography.labelMedium, + text = stringResource(R.string.homepage_standard_module), + style = LegadoTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) } @@ -659,16 +670,10 @@ fun HomepageModuleManageSheet( if (infiniteModules.isNotEmpty()) { item(key = "header_inf_detail") { - HorizontalDivider( - modifier = Modifier.padding( - vertical = 8.dp, - horizontal = 16.dp - ) - ) + PillDivider() AppText( - text = "无限模块槽位", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.secondary, + text = stringResource(R.string.homepage_infinite_module_slot), + style = LegadoTheme.typography.labelMedium, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) } @@ -697,7 +702,7 @@ fun HomepageModuleManageSheet( item(key = "browse_from_set") { SecondaryButton( - text = "浏览书源模块", + text = stringResource(R.string.homepage_browse_source_modules), onClick = { if (setId.startsWith("src_")) { browsingSourceUrl = setId.removePrefix("src_") @@ -746,7 +751,7 @@ fun HomepageModuleManageSheet( state = setsReorderableState, key = set.sourceUrl, title = set.sourceName, - subtitle = "${set.moduleCount} 个模块", + subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount), containerColor = LegadoTheme.colorScheme.onSheetContent, isEnabled = set.isSelected, onToggleSelection = { selectingSetUrl = set.sourceUrl }, @@ -771,14 +776,14 @@ fun HomepageModuleManageSheet( } item(key = "create_set") { SecondaryButton( - text = "+ 新建自定义集", + text = stringResource(R.string.homepage_new_custom_set), onClick = { showCreateSetDialog = true }, modifier = Modifier.fillMaxWidth() ) } item(key = "browse_sources") { SecondaryButton( - text = "浏览书源模块", + text = stringResource(R.string.homepage_browse_source_modules), onClick = { showSourceBrowser = true }, modifier = Modifier.fillMaxWidth() ) @@ -792,7 +797,7 @@ fun HomepageModuleManageSheet( AppAlertDialog( data = renameSetId, onDismissRequest = { renameSetId = null }, - title = "重命名自定义集", + title = stringResource(R.string.homepage_rename_custom_set), content = { setId -> val currentName = remember(setId) { sets.find { it.sourceUrl == setId }?.sourceName ?: "" } @@ -800,7 +805,7 @@ fun HomepageModuleManageSheet( AppTextField( value = tempName, onValueChange = { tempName = it }, - label = "名称", + label = stringResource(R.string.homepage_name_label), modifier = Modifier.fillMaxWidth() ) }, @@ -811,21 +816,21 @@ fun HomepageModuleManageSheet( ) renameSetId = null }, - confirmText = "确定", - dismissText = "取消", + 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 = "新建自定义集", + title = stringResource(R.string.homepage_new_custom_set_title), content = { LaunchedEffect(Unit) { tempName = "" } AppTextField( value = tempName, onValueChange = { tempName = it }, - label = "名称", + label = stringResource(R.string.homepage_name_label), modifier = Modifier.fillMaxWidth() ) }, @@ -833,36 +838,36 @@ fun HomepageModuleManageSheet( if (tempName.isNotBlank()) onCreateCustomSet(tempName) showCreateSetDialog = false }, - confirmText = "确定", - dismissText = "取消", + confirmText = stringResource(R.string.dialog_confirm), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = { showCreateSetDialog = false } ) AppAlertDialog( data = deleteSetConfirmId, onDismissRequest = { deleteSetConfirmId = null }, - title = "删除自定义集", - text = "确定要删除该集及其包含的所有模块副本吗?", + 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 = "删除", - dismissText = "取消", + confirmText = stringResource(R.string.delete), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = { deleteSetConfirmId = null } ) AppAlertDialog( data = deleteConfirmId, onDismissRequest = { deleteConfirmId = null }, - title = "移除模块", - text = "确定要从当前集中移除该模块吗?", + title = stringResource(R.string.homepage_remove_module), + text = stringResource(R.string.homepage_remove_module_confirm), onConfirm = { id -> onDeleteModule(id) deleteConfirmId = null }, - confirmText = "移除", - dismissText = "取消", + confirmText = stringResource(R.string.remove), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = { deleteConfirmId = null } ) @@ -900,13 +905,14 @@ fun HomepageModuleManageSheet( AppAlertDialog( data = if (showAddButtonGroupDialog) Unit else null, onDismissRequest = { showAddButtonGroupDialog = false }, - title = "添加按钮组", + title = stringResource(R.string.homepage_add_button_group), content = { - LaunchedEffect(Unit) { tempButtonGroupTitle = "快捷操作" } + val quickActionsLabel = stringResource(R.string.homepage_quick_actions) + LaunchedEffect(Unit) { tempButtonGroupTitle = quickActionsLabel } AppTextField( value = tempButtonGroupTitle, onValueChange = { tempButtonGroupTitle = it }, - label = "模块标题", + label = stringResource(R.string.homepage_module_title_label), modifier = Modifier.fillMaxWidth() ) }, @@ -920,21 +926,21 @@ fun HomepageModuleManageSheet( selectedKindTitles = emptySet() showAddButtonGroupDialog = false }, - confirmText = "确定", - dismissText = "取消", + confirmText = stringResource(R.string.dialog_confirm), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = { showAddButtonGroupDialog = false } ) AppAlertDialog( data = customSetTitleEdit, onDismissRequest = { customSetTitleEdit = null }, - title = "自定义标题", + title = stringResource(R.string.homepage_custom_title), content = { (_, title) -> LaunchedEffect(title) { titleState = title } AppTextField( value = titleState, onValueChange = { titleState = it }, - label = "标题", + label = stringResource(R.string.homepage_title_label), modifier = Modifier.fillMaxWidth() ) }, @@ -942,8 +948,8 @@ fun HomepageModuleManageSheet( onSetCustomSetTitle(id, titleState.takeIf { it.isNotBlank() }) customSetTitleEdit = null }, - confirmText = "确定", - dismissText = "取消", + confirmText = stringResource(R.string.dialog_confirm), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = { customSetTitleEdit = null } ) } @@ -974,24 +980,11 @@ fun AddCustomModuleDialog( var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) } var showRawLayoutConfig by remember(data) { mutableStateOf(false) } - val layoutKeyConfigs = remember { - mapOf( - "fullWidth" to JsonKeyEditorConfig.Switch, - "showTitle" to JsonKeyEditorConfig.Switch, - "showMore" to JsonKeyEditorConfig.Switch, - "isInfinite" to JsonKeyEditorConfig.Switch, - "aspectRatio" to JsonKeyEditorConfig.Dropdown( - displayEntries = arrayOf("默认", "1:1", "3:4", "2:3", "16:9"), - entryValues = arrayOf("", "1:1", "3:4", "2:3", "16:9") - ) - ) - } - val hasVisualizableKeys = remember(layoutConfig) { runCatching { val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject jsonObject.keySet().any { key -> - key == "columns" || key == "rows" || layoutKeyConfigs.containsKey(key) + key == "columns" || key == "rows" } }.getOrElse { false } } @@ -999,7 +992,9 @@ fun AddCustomModuleDialog( AppAlertDialog( data = data, onDismissRequest = onDismissRequest, - title = if (prefillTitle.isEmpty()) "添加模块" else "编辑模块", + title = if (prefillTitle.isEmpty()) stringResource(R.string.homepage_add_module) else stringResource( + R.string.homepage_edit_module + ), content = { Column( modifier = Modifier @@ -1011,7 +1006,7 @@ fun AddCustomModuleDialog( AppTextField( value = title, onValueChange = { title = it }, - label = "标题", + label = stringResource(R.string.homepage_title_label), modifier = Modifier.fillMaxWidth() ) AppTextField( @@ -1024,7 +1019,7 @@ fun AddCustomModuleDialog( HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } } DropdownListSettingItem( - title = "类型", + title = stringResource(R.string.homepage_type_label), selectedValue = type, displayEntries = typeList.map { it.title }.toTypedArray(), entryValues = typeList.map { it.key }.toTypedArray(), @@ -1037,20 +1032,18 @@ fun AddCustomModuleDialog( modifier = Modifier.fillMaxWidth() ) AppText( - text = "布局配置", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary, + text = stringResource(R.string.homepage_layout_config_label), + style = LegadoTheme.typography.labelMedium, modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) ) if (hasVisualizableKeys) { JsonConfigEditor( jsonString = layoutConfig, onJsonStringChange = { layoutConfig = it }, - keyConfigs = layoutKeyConfigs, modifier = Modifier.fillMaxWidth() ) CompactClickableSettingItem( - title = "编辑原始 JSON (LayoutConfig)", + title = stringResource(R.string.homepage_edit_raw_json), onClick = { showRawLayoutConfig = !showRawLayoutConfig } ) if (showRawLayoutConfig) { @@ -1082,8 +1075,8 @@ fun AddCustomModuleDialog( ) ) }, - confirmText = "确定", - dismissText = "取消", + confirmText = stringResource(R.string.dialog_confirm), + dismissText = stringResource(R.string.dialog_cancel), onDismiss = onDismissRequest ) } 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 eabaa9403..987ba600a 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 @@ -44,11 +44,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.legado.app.R import io.legado.app.domain.model.HomepageModuleType import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.main.homepage.modules.BannerModule @@ -102,6 +104,7 @@ fun HomepageScreen( }) val mixedGridState = rememberLazyStaggeredGridState() + val homeString = stringResource(R.string.home) val currentTitle by remember( layoutMode, pagerState.currentPage, @@ -110,16 +113,16 @@ fun HomepageScreen( ) { derivedStateOf { if (layoutMode == 1) { - "首页" + homeString } else { val firstHeader = mixedGridState.layoutInfo.visibleItemsInfo.firstOrNull { (it.key as? String)?.startsWith("header_") == true } if (firstHeader != null) { val id = (firstHeader.key as? String).orEmpty().substringAfter("header_", "") - uiState.modules.find { it.globalId == id }?.setName ?: "首页" + uiState.modules.find { it.globalId == id }?.setName ?: homeString } else { - "首页" + homeString } } } @@ -198,7 +201,7 @@ fun HomepageScreen( } else { if (selectedSets.isEmpty()) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - AppText("未选择任何书源集") + AppText(stringResource(R.string.homepage_no_source_sets_selected)) } } else { HorizontalPager( @@ -234,13 +237,13 @@ fun HomepageScreen( AppAlertDialog( data = errorMsg, onDismissRequest = { errorMsg = null }, - title = "模块错误", - confirmText = "复制", + title = stringResource(R.string.homepage_module_error), + confirmText = stringResource(R.string.copy_text), onConfirm = { context.sendToClip(it) errorMsg = null }, - dismissText = "关闭", + dismissText = stringResource(R.string.close), onDismiss = { errorMsg = null } ) @@ -316,7 +319,7 @@ private fun ModuleList( ) { if (modules.isEmpty()) { Box(modifier = modifier, contentAlignment = Alignment.Center) { - AppText("请在书源中添加首页模块定义") + AppText(stringResource(R.string.homepage_add_module_definition)) } } else { // 1. 过滤和重排模块:每个集只能有一个无限流模块,且必须在最下面 @@ -403,7 +406,7 @@ private fun ModuleList( ) Spacer(modifier = Modifier.height(4.dp)) SecondaryButton( - text = "重试", + text = stringResource(R.string.retry), onClick = { viewModel.retryModule(moduleUi.globalId) } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageSourceSelectSheet.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageSourceSelectSheet.kt index 58bb0e5dc..520264995 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageSourceSelectSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageSourceSelectSheet.kt @@ -45,7 +45,7 @@ fun HomepageSourceSelectSheet( AppModalBottomSheet( show = show, onDismissRequest = onDismissRequest, - title = "筛选书源", + title = stringResource(R.string.homepage_filter_sources), ) { Column { SearchBar( 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 8cc1edf18..8bdf63423 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 @@ -2,6 +2,7 @@ package io.legado.app.ui.main.homepage import android.app.Application import androidx.lifecycle.viewModelScope +import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook @@ -90,6 +91,7 @@ class HomepageViewModel( private val localModulesFlow = gateway.flowEnabled() private val _bookSourcesCache = MutableStateFlow>(emptyMap()) + private val _layoutConfigCache = MutableStateFlow>>(emptyMap()) val allModulesCache = gateway.flowAll() .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -115,8 +117,8 @@ class HomepageViewModel( ) { grouped, contentStates, flags, sourcesCache, customSets -> val setNames = customSets.associate { it.id to it.name } val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id } + val configCache = _layoutConfigCache.value - // 按照集排序设置来排布模块 val displayModules = sortedSetIds.flatMap { setId -> val setUrl = customSetUrl(setId) val mods = grouped[setUrl] ?: emptyList() @@ -125,17 +127,7 @@ class HomepageViewModel( val sourceName = source?.bookSourceName ?: module.sourceUrl val setName = module.customSetId?.let { setNames[it] } ?: sourceName val exploreUrl = module.url ?: source?.exploreUrl - - val configMap = mutableMapOf() - module.layoutConfig?.let { configStr -> - try { - val json = GSON.fromJson(configStr, Map::class.java) - json?.forEach { (k, v) -> - configMap["layout_$k"] = v.toString() - } - } catch (_: Exception) { - } - } + val configMap = configCache[module.id] ?: emptyMap() HomepageModuleUi( sourceUrl = module.sourceUrl, @@ -203,6 +195,26 @@ class HomepageViewModel( private val _pendingUserModules = MutableStateFlow>(emptyList()) init { + // 解析并缓存模块 layoutConfig,避免在 combine 中重复解析 + viewModelScope.launch { + localModulesFlow.collect { modules -> + val cache = mutableMapOf>() + for (module in modules) { + val configStr = module.layoutConfig ?: continue + try { + val json = GSON.fromJson(configStr, Map::class.java) + if (json != null) { + val map = mutableMapOf() + json.forEach { (k, v) -> map["layout_$k"] = v.toString() } + cache[module.id] = map + } + } catch (_: Exception) { + } + } + _layoutConfigCache.value = cache + } + } + // sync: 只处理有 homepageModules 的书源 viewModelScope.launch { initModulesSyncFlow.collect { sources -> @@ -438,7 +450,14 @@ class HomepageViewModel( states[globalId] as? ModuleLoadState.Loaded ?: return@update states states + (globalId to lastState.copy(isLoadingMore = false)) } - _effects.tryEmit(HomepageEffect.ShowSnackbar("加载更多失败: ${e.message}")) + _effects.tryEmit( + HomepageEffect.ShowSnackbar( + getApplication().getString( + R.string.homepage_load_more_failed, + e.message ?: "" + ) + ) + ) } } } @@ -529,7 +548,13 @@ class HomepageViewModel( } if (hasInfinite) { viewModelScope.launch { - _effects.emit(HomepageEffect.ShowSnackbar("该分组已存在无限加载模块")) + _effects.emit( + HomepageEffect.ShowSnackbar( + getApplication().getString( + R.string.homepage_module_duplicate_infinite + ) + ) + ) } return } @@ -603,19 +628,18 @@ class HomepageViewModel( fun reorderJoinedModules(orderedIds: List) { viewModelScope.launch { - orderedIds.forEachIndexed { index, id -> - gateway.setSortOrder(id, index) - } + val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap() + gateway.batchSetSortOrders(orders) notifyConfigChanged() } } fun reorderCustomSets(orderedUrls: List) { viewModelScope.launch { - orderedUrls.forEachIndexed { index, url -> - val id = customSetIdFromUrl(url) - gateway.setCustomSetSortOrder(id, index) - } + val orders = orderedUrls.mapIndexed { index, url -> + customSetIdFromUrl(url) to index + }.toMap() + gateway.batchSetCustomSetSortOrders(orders) notifyConfigChanged() } } @@ -749,7 +773,13 @@ class HomepageViewModel( } if (hasInfinite) { viewModelScope.launch { - _effects.emit(HomepageEffect.ShowSnackbar("该分组已存在无限加载模块")) + _effects.emit( + HomepageEffect.ShowSnackbar( + getApplication().getString( + R.string.homepage_module_duplicate_infinite + ) + ) + ) } return } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt index 4eab8e64a..940a3faaf 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt @@ -133,7 +133,7 @@ fun ButtonGroupModule( verticalArrangement = Arrangement.Center, modifier = Modifier .fillMaxSize() - .padding(vertical = 12.dp, horizontal = 4.dp) + .padding(vertical = 8.dp, horizontal = 4.dp) ) { if (hasIcon) { SourceIcon( @@ -143,12 +143,12 @@ fun ButtonGroupModule( } ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(6.dp)) } AppText( text = displayName, - style = LegadoTheme.typography.labelMedium, + style = LegadoTheme.typography.labelSmallEmphasized, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Clip, diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt index 549ab4996..33d47075d 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt @@ -22,10 +22,12 @@ import androidx.compose.runtime.saveable.rememberSaveable 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.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import io.legado.app.R import io.legado.app.data.entities.SearchBook import io.legado.app.domain.model.BookShelfState import io.legado.app.ui.main.bookCoverSharedElementKey @@ -93,7 +95,9 @@ fun RankingModule( modifier = Modifier.size(20.dp) ) AppText( - text = if (isExpanded) "收起" else "显示全部", + text = if (isExpanded) stringResource(R.string.homepage_collapse) else stringResource( + R.string.homepage_show_all + ), style = LegadoTheme.typography.labelMediumEmphasized, color = if (isExpanded) LegadoTheme.colorScheme.outline else LegadoTheme.colorScheme.primary, modifier = Modifier.padding(start = 4.dp) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt index bea491e26..1b42add4d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt @@ -77,8 +77,8 @@ fun CoilBookCover( } val hasCustomDefault = !randomPath.isNullOrBlank() - var isOnlineCoverLoaded by remember(path) { - mutableStateOf(false) + var isOnlineCoverLoaded by remember(path, sharedCoverKey, finalPath) { + mutableStateOf(sharedCoverKey != null && finalPath != null) } Box( diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c6efa5e08..ad3f6d20f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1674,4 +1674,63 @@ 显示WebView日志 优先缓存 无限制 + 加载更多失败: %1$s + 该分组已存在无限加载模块 + 快捷操作 + 添加模块 + 模块列表 + 浏览书源模块 + 集详情 + 首页模块管理 + 全部分组 + 已加入 + 书源模块 + 发现 + 暂无已加入的模块 + 标准模块 (可拖拽排序) + 底栏无限模块 + 当前生效 + 已被屏蔽 + 该书源的 homepageModules JSON 为空 + 已加入 + 模块类型 + 该书源暂无发现项 + 选择项 + + 手动添加 + %1$d 个模块 + 暂无模块 + 浏览书源模块添加 + 标准模块 + 无限模块槽位 + + 新建自定义集 + 重命名自定义集 + 名称 + 新建自定义集 + 删除自定义集 + 确定要删除该集及其包含的所有模块副本吗? + 移除模块 + 确定要从当前集中移除该模块吗? + 添加按钮组 + 模块标题 + 自定义标题 + 标题 + 编辑模块 + 类型 + 布局配置 + 编辑原始 JSON (LayoutConfig) + 布局设置 + 首页布局模式 + 混合列表 + 分源Tab + 筛选书源 + 未选择任何书源集 + 模块错误 + 请在书源中添加首页模块定义 + 收起 + 显示全部 + 默认 + 移除 + · 当前生效 + · 已被屏蔽 + · 已加入 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f6cbf1a74..5e1ef53e6 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1312,4 +1312,63 @@ 优先缓存 无限制 %1$d 本 + 載入更多失敗: %1$s + 該分組已存在無限載入模塊 + 快捷操作 + 添加模塊 + 模塊列表 + 瀏覽書源模塊 + 集詳情 + 首頁模塊管理 + 全部分組 + 已加入 + 書源模塊 + 發現 + 暫無已加入的模塊 + 標準模塊 (可拖拽排序) + 底欄無限模塊 + 當前生效 + 已被屏蔽 + 該書源的 homepageModules JSON 為空 + 已加入 + 模塊類型 + 該書源暫無發現項 + 選擇項 + + 手動添加 + %1$d 個模塊 + 暫無模塊 + 瀏覽書源模塊添加 + 標準模塊 + 無限模塊槽位 + + 新建自定義集 + 重命名自定義集 + 名稱 + 新建自定義集 + 刪除自定義集 + 確定要刪除該集及其包含的所有模塊副本嗎? + 移除模塊 + 確定要從當前集中移除該模塊嗎? + 添加按鈕組 + 模塊標題 + 自定義標題 + 標題 + 編輯模塊 + 類型 + 佈局配置 + 編輯原始 JSON (LayoutConfig) + 佈局設置 + 首頁佈局模式 + 混合列表 + 分源Tab + 篩選書源 + 未選擇任何書源集 + 模塊錯誤 + 請在書源中添加首頁模塊定義 + 收起 + 顯示全部 + 預設 + 移除 + · 當前生效 + · 已被屏蔽 + · 已加入 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index cfe62e79a..47c18be04 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1315,4 +1315,63 @@ 优先缓存 无限制 %1$d 本 + 載入更多失敗: %1$s + 該分組已存在無限載入模組 + 快捷操作 + 添加模組 + 模組列表 + 瀏覽書源模組 + 集詳情 + 首頁模組管理 + 全部分組 + 已加入 + 書源模組 + 發現 + 暫無已加入的模組 + 標準模組 (可拖拽排序) + 底欄無限模組 + 當前生效 + 已被屏蔽 + 該書源的 homepageModules JSON 為空 + 已加入 + 模組類型 + 該書源暫無發現項 + 選擇項 + + 手動添加 + %1$d 個模組 + 暫無模組 + 瀏覽書源模組添加 + 標準模組 + 無限模組槽位 + + 新建自定義集 + 重命名自定義集 + 名稱 + 新建自定義集 + 刪除自定義集 + 確定要刪除該集及其包含的所有模組副本嗎? + 移除模組 + 確定要從當前集中移除該模組嗎? + 添加按鈕組 + 模組標題 + 自定義標題 + 標題 + 編輯模組 + 類型 + 佈局配置 + 編輯原始 JSON (LayoutConfig) + 佈局設置 + 首頁佈局模式 + 混合列表 + 分源Tab + 篩選書源 + 未選擇任何書源集 + 模組錯誤 + 請在書源中添加首頁模組定義 + 收起 + 顯示全部 + 預設 + 移除 + · 目前生效 + · 已被封鎖 + · 已加入 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e10e98775..44d1ec46b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1680,4 +1680,63 @@ Download Image Cache Network + Failed to load more: %1$s + This group already has an infinite loading module + Quick Actions + Add Module + Module List + Browse Source Modules + Set Detail + Homepage Module Manager + All Groups + Joined + Source Modules + Discover + No joined modules + Standard Modules (Drag to reorder) + Bottom Infinite Module + In effect + Blocked + The source\'s homepageModules JSON is empty + Already joined + Module Type + This source has no discover items + Select Items + + Manual Add + %1$d modules + No modules + Browse Source Modules to Add + Standard Module + Infinite Module Slot + + New Custom Set + Rename Custom Set + Name + New Custom Set + Delete Custom Set + Are you sure you want to delete this set and all its module copies? + Remove Module + Are you sure you want to remove this module from the current set? + Add Button Group + Module Title + Custom Title + Title + Edit Module + Type + Layout Config + Edit Raw JSON (LayoutConfig) + Layout Settings + Homepage Layout Mode + Mixed List + Tabs by Source + Filter Sources + No source sets selected + Module Error + Please add homepage module definitions in the book source + Collapse + Show All + Default + Remove + · In effect + · Blocked + · Already joined