HapeLee 5 месяцев назад
Родитель
Сommit
bbec36bf36

+ 1 - 2
.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\\\\\", \"\"\\) })"
     ]
   }
 }

+ 77 - 202
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 数组,每个元素定义一个模块:
+### 模块通用字段
 
-```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 — 网格书架
+| 字段               | 类型       | 必须 | 说明                                                    |
+|:-----------------|:---------|:---|:------------------------------------------------------|
+| **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)。 |
 
-网格布局,适合展示分类书单。支持自定义行列。
+---
 
-- `columns`: 默认 3
-- `rows`: 默认 2。若设置为 `0`,则显示为平铺列表并支持下拉加载更多。
+## 2. 模块类型 (Module Types)
 
-```json
-{
-  "key": "scifi_grid",
-  "type": "grid",
-  "title": "科幻精选",
-  "kindTitle": "科幻",
-  "layoutConfig": {
-    "columns": 3,
-    "rows": 2
-  }
-}
-```
-
-### card — 推荐卡片
+### 列表与轮播类
 
-大图横向滑动卡片,展示封面 + 书名 + 简介。适合需要更多信息展示的场景。
-
-```json
-{
-  "key": "editor_pick",
-  "type": "card",
-  "title": "编辑推荐",
-  "kindTitle": "编辑推荐"
-}
-```
+| 类型 (Type) | 描述    | 特点                     |
+|:----------|:------|:-----------------------|
+| `banner`  | 横滑轮播图 | 适合展示高权重的精品推荐,使用大图封面。   |
+| `ranking` | 排行榜列表 | 垂直列表展示,带排名序号。          |
+| `card`    | 推荐卡片  | 横向滑动的卡片流,同时显示封面、标题及简介。 |
 
-### waterfall — 错位瀑布流
+### 网格类
 
-瀑布流布局,展示封面 + 书名 + 简介。支持 `layoutConfig.columns` 自定义列数(默认 2)。支持无限加载更多。
+| 类型 (Type)      | 描述    | 特点                |
+|:---------------|:------|:------------------|
+| `grid`         | 标准网格  | 最常用的展示形式。支持自定义行列。 |
+| `gridRanking`  | 网格排行榜 | 多行多列的排行展示。横向翻页。   |
+| `infiniteGrid` | 无限网格  | 垂直滚动的网格流。无限加载。    |
+| `waterfall`    | 错位瀑布流 | 垂直错位排列的书架流。无限加载。  |
 
-```json
-{
-  "key": "hot_wf",
-  "type": "waterfall",
-  "title": "大家都在看",
-  "kindTitle": "热门",
-  "layoutConfig": {
-    "columns": 2
-  }
-}
-```
+### 功能类
 
-### buttonGroup — 按钮组
+| 类型 (Type)     | 描述    | 特点                                          |
+|:--------------|:------|:--------------------------------------------|
+| `buttonGroup` | 快捷按钮组 | 渲染为一组圆形/图标按钮,支持自动填充宽度与自动分列。通常用于放置常用分类或功能入口。 |
 
-显示为网格排列的分类按钮。适合放置常用的分类或动作入口。
+---
 
-- **布局特性**:自动平衡每行按钮数量(例如 6 个按钮显示为 3+3,8 个显示为 4+4)。每行最多 5 个。
-- **args**: 可选。由分类标题组成的 JSON 数组字符串,如 `["排行", "分类", "完本"]`。若不填则默认显示该书源前
-  5 个分类。
-- **layoutConfig**:
-    - `icon`: 全局默认图标。支持网络图片 URL。
-    - `icons`: 图标映射对象。以分类标题为键,网络图片 URL 为值。
+## 3. 布局配置 (LayoutConfig)
 
-```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"
-    }
-  }
-}
-```
+通过 `layoutConfig` 对象,可以精细化控制模块的表现。
 
-## kindTitle 匹配规则
+| 属性 (Property) | 类型       | 适用类型                                | 默认值 | 说明                                       |
+|:--------------|:---------|:------------------------------------|:----|:-----------------------------------------|
+| `columns`     | `Int`    | `grid`, `waterfall`, `infiniteGrid` | 3   | 每行显示的列数。                                 |
+| `icon`        | `String` | `buttonGroup`                       | -   | 按钮组的默认统一图标 URL。                          |
+| `icons`       | `Object` | `buttonGroup`                       | -   | 图标映射表。例:`{"排行": "http://path/to/icon"}`。 |
 
-模块加载时,系统会根据 `kindTitle` 在书源的「发现」分类列表中进行匹配:
+---
 
-1. 在 `exploreKinds()`(即发现页的分类列表)中查找 `title` 等于 `kindTitle` 的分类
-2. 如果匹配成功,使用该分类的 `url` 来加载数据
-3. 如果匹配失败,或 `kindTitle` 为空,则使用书源的默认 `exploreUrl`
-4. 如果两个 URL 都没有,该模块将显示加载错误
+## 4. 数据绑定逻辑 (Data Binding)
 
-**建议**:确保 `kindTitle` 的值与发现页分类的标题**完全一致**(包括大小写和标点)。
+1. **自动匹配**:如果提供了 `kindTitle`,系统会遍历书源 `exploreKinds()` 返回的列表。如果某个分类的
+   `title` 与之完全一致,该模块将自动使用该分类的 `url`。
+2. **静态指定**:如果提供了 `url`,系统将直接请求该 URL。
+3. **降级逻辑**:若 `kindTitle` 未匹配且无 `url`,模块将回退至书源的主 `exploreUrl`。
 
-## 完整示例
+---
 
-以下是一个书源配置了多种模块的完整 JSON:
+## 5. 完整 JSON 示例
 
 ```json
 [
   {
-    "key": "hot",
+    "key": "top_banner",
     "type": "banner",
-    "title": "热门推荐",
-    "kindTitle": "热门",
-    "displayCount": 6
+    "title": "精品强推",
+    "kindTitle": "首页推荐"
   },
   {
-    "key": "rank_week",
-    "type": "ranking",
-    "title": "周榜",
-    "kindTitle": "周排行",
-    "displayCount": 10
+    "key": "quick_nav",
+    "type": "buttonGroup",
+    "title": "分类导航",
+    "args": "[\"武侠\", \"仙侠\", \"都市\", \"历史\"]",
+    "layoutConfig": {
+      "icon": "https://example.com/icons/default.png",
+      "icons": {
+        "武侠": "https://example.com/icons/wuxia.png"
+      }
+    }
   },
   {
-    "key": "rank_month",
+    "key": "hot_rank",
     "type": "ranking",
-    "title": "月榜",
-    "kindTitle": "月排行",
-    "displayCount": 10
-  },
-  {
-    "key": "scifi",
-    "type": "grid",
-    "title": "科幻精选",
-    "kindTitle": "科幻",
-    "displayCount": 6
+    "title": "热门榜单",
+    "kindTitle": "排行榜",
+    "layoutConfig": {
+      "rows": 5
+    }
   },
   {
-    "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` 用于区分模块所属的集(书源集或自定义集)。

+ 5 - 0
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<String, Int>) {
+        orders.forEach { (id, order) -> setSortOrder(id, order) }
+    }
+
     @Query("DELETE FROM homepage_custom_sets WHERE id = :id")
     suspend fun delete(id: String)
 }

+ 5 - 0
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<String, Int>) {
+        orders.forEach { (id, order) -> setSortOrder(id, order) }
+    }
+
     @Query("UPDATE homepage_modules SET customSetTitle = :title WHERE id = :id")
     suspend fun setCustomSetTitle(id: String, title: String?)
 

+ 4 - 0
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<String, Int>) =
+        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<String, Int>) =
+        customSetDao.batchSetSortOrders(orders)
 
     override suspend fun createCustomSet(name: String): CustomSetItem {
         val entity = HomepageCustomSet(

+ 2 - 0
app/src/main/java/io/legado/app/domain/gateway/HomepageModulesGateway.kt

@@ -15,6 +15,7 @@ interface HomepageModulesGateway {
     suspend fun upsertAll(modules: List<ModuleItem>)
     suspend fun setEnabled(id: String, enabled: Boolean)
     suspend fun setSortOrder(id: String, order: Int)
+    suspend fun batchSetSortOrders(orders: Map<String, Int>)
     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<String, Int>)
     suspend fun createCustomSet(name: String): CustomSetItem
     suspend fun renameCustomSet(id: String, name: String)
     suspend fun deleteCustomSet(id: String)

+ 10 - 10
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)

+ 8 - 3
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 <T> 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()) }
             )

+ 89 - 96
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 <T> HomepageModuleManageSheet(
     var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> HomepageModuleManageSheet(
             AppTextField(
                 value = tempName,
                 onValueChange = { tempName = it },
-                label = "名称",
+                label = stringResource(R.string.homepage_name_label),
                 modifier = Modifier.fillMaxWidth()
             )
         },
@@ -811,21 +816,21 @@ fun <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> 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 <T> AddCustomModuleDialog(
                 AppTextField(
                     value = title,
                     onValueChange = { title = it },
-                    label = "标题",
+                    label = stringResource(R.string.homepage_title_label),
                     modifier = Modifier.fillMaxWidth()
                 )
                 AppTextField(
@@ -1024,7 +1019,7 @@ fun <T> 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 <T> 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 <T> AddCustomModuleDialog(
                 )
             )
         },
-        confirmText = "确定",
-        dismissText = "取消",
+        confirmText = stringResource(R.string.dialog_confirm),
+        dismissText = stringResource(R.string.dialog_cancel),
         onDismiss = onDismissRequest
     )
 }

+ 12 - 9
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)
                                     }

+ 1 - 1
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(

+ 52 - 22
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<Map<String, BookSource>>(emptyMap())
+    private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(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<String, String>()
-                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<List<ModuleItem>>(emptyList())
 
     init {
+        // 解析并缓存模块 layoutConfig,避免在 combine 中重复解析
+        viewModelScope.launch {
+            localModulesFlow.collect { modules ->
+                val cache = mutableMapOf<String, Map<String, String>>()
+                for (module in modules) {
+                    val configStr = module.layoutConfig ?: continue
+                    try {
+                        val json = GSON.fromJson(configStr, Map::class.java)
+                        if (json != null) {
+                            val map = mutableMapOf<String, String>()
+                            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<Application>().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<Application>().getString(
+                                R.string.homepage_module_duplicate_infinite
+                            )
+                        )
+                    )
                 }
                 return
             }
@@ -603,19 +628,18 @@ class HomepageViewModel(
 
     fun reorderJoinedModules(orderedIds: List<String>) {
         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<String>) {
         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<Application>().getString(
+                                R.string.homepage_module_duplicate_infinite
+                            )
+                        )
+                    )
                 }
                 return
             }

+ 3 - 3
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,

+ 5 - 1
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)

+ 2 - 2
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(

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

@@ -1674,4 +1674,63 @@
     <string name="load_with_web_log">显示WebView日志</string>
     <string name="cache_first">优先缓存</string>
     <string name="refresh_limit_unlimited">无限制</string>
+    <string name="homepage_load_more_failed">加载更多失败: %1$s</string>
+    <string name="homepage_module_duplicate_infinite">该分组已存在无限加载模块</string>
+    <string name="homepage_quick_actions">快捷操作</string>
+    <string name="homepage_add_module">添加模块</string>
+    <string name="homepage_module_list">模块列表</string>
+    <string name="homepage_browse_source_modules">浏览书源模块</string>
+    <string name="homepage_set_detail">集详情</string>
+    <string name="homepage_module_manage">首页模块管理</string>
+    <string name="homepage_all_groups">全部分组</string>
+    <string name="homepage_tab_joined">已加入</string>
+    <string name="homepage_tab_source_modules">书源模块</string>
+    <string name="homepage_tab_discover">发现</string>
+    <string name="homepage_no_joined_modules">暂无已加入的模块</string>
+    <string name="homepage_standard_modules_sortable">标准模块 (可拖拽排序)</string>
+    <string name="homepage_infinite_module_bottom">底栏无限模块</string>
+    <string name="homepage_in_effect">当前生效</string>
+    <string name="homepage_blocked">已被屏蔽</string>
+    <string name="homepage_source_json_empty">该书源的 homepageModules JSON 为空</string>
+    <string name="homepage_already_joined">已加入</string>
+    <string name="homepage_module_type">模块类型</string>
+    <string name="homepage_source_no_discover">该书源暂无发现项</string>
+    <string name="homepage_select_items">选择项</string>
+    <string name="homepage_manual_add">+ 手动添加</string>
+    <string name="homepage_n_modules">%1$d 个模块</string>
+    <string name="homepage_no_modules">暂无模块</string>
+    <string name="homepage_browse_to_add">浏览书源模块添加</string>
+    <string name="homepage_standard_module">标准模块</string>
+    <string name="homepage_infinite_module_slot">无限模块槽位</string>
+    <string name="homepage_new_custom_set">+ 新建自定义集</string>
+    <string name="homepage_rename_custom_set">重命名自定义集</string>
+    <string name="homepage_name_label">名称</string>
+    <string name="homepage_new_custom_set_title">新建自定义集</string>
+    <string name="homepage_delete_custom_set">删除自定义集</string>
+    <string name="homepage_delete_custom_set_confirm">确定要删除该集及其包含的所有模块副本吗?</string>
+    <string name="homepage_remove_module">移除模块</string>
+    <string name="homepage_remove_module_confirm">确定要从当前集中移除该模块吗?</string>
+    <string name="homepage_add_button_group">添加按钮组</string>
+    <string name="homepage_module_title_label">模块标题</string>
+    <string name="homepage_custom_title">自定义标题</string>
+    <string name="homepage_title_label">标题</string>
+    <string name="homepage_edit_module">编辑模块</string>
+    <string name="homepage_type_label">类型</string>
+    <string name="homepage_layout_config_label">布局配置</string>
+    <string name="homepage_edit_raw_json">编辑原始 JSON (LayoutConfig)</string>
+    <string name="homepage_layout_settings">布局设置</string>
+    <string name="homepage_layout_mode">首页布局模式</string>
+    <string name="homepage_layout_mixed">混合列表</string>
+    <string name="homepage_layout_tabs">分源Tab</string>
+    <string name="homepage_filter_sources">筛选书源</string>
+    <string name="homepage_no_source_sets_selected">未选择任何书源集</string>
+    <string name="homepage_module_error">模块错误</string>
+    <string name="homepage_add_module_definition">请在书源中添加首页模块定义</string>
+    <string name="homepage_collapse">收起</string>
+    <string name="homepage_show_all">显示全部</string>
+    <string name="homepage_default">默认</string>
+    <string name="remove">移除</string>
+    <string name="homepage_status_in_effect"> · 当前生效</string>
+    <string name="homepage_status_blocked"> · 已被屏蔽</string>
+    <string name="homepage_status_joined"> · 已加入</string>
 </resources>

+ 59 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1312,4 +1312,63 @@
     <string name="cache_first">优先缓存</string>
     <string name="refresh_limit_unlimited">无限制</string>
     <string name="refresh_limit_books">%1$d 本</string>
+    <string name="homepage_load_more_failed">載入更多失敗: %1$s</string>
+    <string name="homepage_module_duplicate_infinite">該分組已存在無限載入模塊</string>
+    <string name="homepage_quick_actions">快捷操作</string>
+    <string name="homepage_add_module">添加模塊</string>
+    <string name="homepage_module_list">模塊列表</string>
+    <string name="homepage_browse_source_modules">瀏覽書源模塊</string>
+    <string name="homepage_set_detail">集詳情</string>
+    <string name="homepage_module_manage">首頁模塊管理</string>
+    <string name="homepage_all_groups">全部分組</string>
+    <string name="homepage_tab_joined">已加入</string>
+    <string name="homepage_tab_source_modules">書源模塊</string>
+    <string name="homepage_tab_discover">發現</string>
+    <string name="homepage_no_joined_modules">暫無已加入的模塊</string>
+    <string name="homepage_standard_modules_sortable">標準模塊 (可拖拽排序)</string>
+    <string name="homepage_infinite_module_bottom">底欄無限模塊</string>
+    <string name="homepage_in_effect">當前生效</string>
+    <string name="homepage_blocked">已被屏蔽</string>
+    <string name="homepage_source_json_empty">該書源的 homepageModules JSON 為空</string>
+    <string name="homepage_already_joined">已加入</string>
+    <string name="homepage_module_type">模塊類型</string>
+    <string name="homepage_source_no_discover">該書源暫無發現項</string>
+    <string name="homepage_select_items">選擇項</string>
+    <string name="homepage_manual_add">+ 手動添加</string>
+    <string name="homepage_n_modules">%1$d 個模塊</string>
+    <string name="homepage_no_modules">暫無模塊</string>
+    <string name="homepage_browse_to_add">瀏覽書源模塊添加</string>
+    <string name="homepage_standard_module">標準模塊</string>
+    <string name="homepage_infinite_module_slot">無限模塊槽位</string>
+    <string name="homepage_new_custom_set">+ 新建自定義集</string>
+    <string name="homepage_rename_custom_set">重命名自定義集</string>
+    <string name="homepage_name_label">名稱</string>
+    <string name="homepage_new_custom_set_title">新建自定義集</string>
+    <string name="homepage_delete_custom_set">刪除自定義集</string>
+    <string name="homepage_delete_custom_set_confirm">確定要刪除該集及其包含的所有模塊副本嗎?</string>
+    <string name="homepage_remove_module">移除模塊</string>
+    <string name="homepage_remove_module_confirm">確定要從當前集中移除該模塊嗎?</string>
+    <string name="homepage_add_button_group">添加按鈕組</string>
+    <string name="homepage_module_title_label">模塊標題</string>
+    <string name="homepage_custom_title">自定義標題</string>
+    <string name="homepage_title_label">標題</string>
+    <string name="homepage_edit_module">編輯模塊</string>
+    <string name="homepage_type_label">類型</string>
+    <string name="homepage_layout_config_label">佈局配置</string>
+    <string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
+    <string name="homepage_layout_settings">佈局設置</string>
+    <string name="homepage_layout_mode">首頁佈局模式</string>
+    <string name="homepage_layout_mixed">混合列表</string>
+    <string name="homepage_layout_tabs">分源Tab</string>
+    <string name="homepage_filter_sources">篩選書源</string>
+    <string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
+    <string name="homepage_module_error">模塊錯誤</string>
+    <string name="homepage_add_module_definition">請在書源中添加首頁模塊定義</string>
+    <string name="homepage_collapse">收起</string>
+    <string name="homepage_show_all">顯示全部</string>
+    <string name="homepage_default">預設</string>
+    <string name="remove">移除</string>
+    <string name="homepage_status_in_effect"> · 當前生效</string>
+    <string name="homepage_status_blocked"> · 已被屏蔽</string>
+    <string name="homepage_status_joined"> · 已加入</string>
 </resources>

+ 59 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1315,4 +1315,63 @@
     <string name="cache_first">优先缓存</string>
     <string name="refresh_limit_unlimited">无限制</string>
     <string name="refresh_limit_books">%1$d 本</string>
+    <string name="homepage_load_more_failed">載入更多失敗: %1$s</string>
+    <string name="homepage_module_duplicate_infinite">該分組已存在無限載入模組</string>
+    <string name="homepage_quick_actions">快捷操作</string>
+    <string name="homepage_add_module">添加模組</string>
+    <string name="homepage_module_list">模組列表</string>
+    <string name="homepage_browse_source_modules">瀏覽書源模組</string>
+    <string name="homepage_set_detail">集詳情</string>
+    <string name="homepage_module_manage">首頁模組管理</string>
+    <string name="homepage_all_groups">全部分組</string>
+    <string name="homepage_tab_joined">已加入</string>
+    <string name="homepage_tab_source_modules">書源模組</string>
+    <string name="homepage_tab_discover">發現</string>
+    <string name="homepage_no_joined_modules">暫無已加入的模組</string>
+    <string name="homepage_standard_modules_sortable">標準模組 (可拖拽排序)</string>
+    <string name="homepage_infinite_module_bottom">底欄無限模組</string>
+    <string name="homepage_in_effect">當前生效</string>
+    <string name="homepage_blocked">已被屏蔽</string>
+    <string name="homepage_source_json_empty">該書源的 homepageModules JSON 為空</string>
+    <string name="homepage_already_joined">已加入</string>
+    <string name="homepage_module_type">模組類型</string>
+    <string name="homepage_source_no_discover">該書源暫無發現項</string>
+    <string name="homepage_select_items">選擇項</string>
+    <string name="homepage_manual_add">+ 手動添加</string>
+    <string name="homepage_n_modules">%1$d 個模組</string>
+    <string name="homepage_no_modules">暫無模組</string>
+    <string name="homepage_browse_to_add">瀏覽書源模組添加</string>
+    <string name="homepage_standard_module">標準模組</string>
+    <string name="homepage_infinite_module_slot">無限模組槽位</string>
+    <string name="homepage_new_custom_set">+ 新建自定義集</string>
+    <string name="homepage_rename_custom_set">重命名自定義集</string>
+    <string name="homepage_name_label">名稱</string>
+    <string name="homepage_new_custom_set_title">新建自定義集</string>
+    <string name="homepage_delete_custom_set">刪除自定義集</string>
+    <string name="homepage_delete_custom_set_confirm">確定要刪除該集及其包含的所有模組副本嗎?</string>
+    <string name="homepage_remove_module">移除模組</string>
+    <string name="homepage_remove_module_confirm">確定要從當前集中移除該模組嗎?</string>
+    <string name="homepage_add_button_group">添加按鈕組</string>
+    <string name="homepage_module_title_label">模組標題</string>
+    <string name="homepage_custom_title">自定義標題</string>
+    <string name="homepage_title_label">標題</string>
+    <string name="homepage_edit_module">編輯模組</string>
+    <string name="homepage_type_label">類型</string>
+    <string name="homepage_layout_config_label">佈局配置</string>
+    <string name="homepage_edit_raw_json">編輯原始 JSON (LayoutConfig)</string>
+    <string name="homepage_layout_settings">佈局設置</string>
+    <string name="homepage_layout_mode">首頁佈局模式</string>
+    <string name="homepage_layout_mixed">混合列表</string>
+    <string name="homepage_layout_tabs">分源Tab</string>
+    <string name="homepage_filter_sources">篩選書源</string>
+    <string name="homepage_no_source_sets_selected">未選擇任何書源集</string>
+    <string name="homepage_module_error">模組錯誤</string>
+    <string name="homepage_add_module_definition">請在書源中添加首頁模組定義</string>
+    <string name="homepage_collapse">收起</string>
+    <string name="homepage_show_all">顯示全部</string>
+    <string name="homepage_default">預設</string>
+    <string name="remove">移除</string>
+    <string name="homepage_status_in_effect"> · 目前生效</string>
+    <string name="homepage_status_blocked"> · 已被封鎖</string>
+    <string name="homepage_status_joined"> · 已加入</string>
 </resources>

+ 59 - 0
app/src/main/res/values/strings.xml

@@ -1680,4 +1680,63 @@
     <string name="download_setting">Download</string>
     <string name="image_cache">Image Cache</string>
     <string name="network">Network</string>
+    <string name="homepage_load_more_failed">Failed to load more: %1$s</string>
+    <string name="homepage_module_duplicate_infinite">This group already has an infinite loading module</string>
+    <string name="homepage_quick_actions">Quick Actions</string>
+    <string name="homepage_add_module">Add Module</string>
+    <string name="homepage_module_list">Module List</string>
+    <string name="homepage_browse_source_modules">Browse Source Modules</string>
+    <string name="homepage_set_detail">Set Detail</string>
+    <string name="homepage_module_manage">Homepage Module Manager</string>
+    <string name="homepage_all_groups">All Groups</string>
+    <string name="homepage_tab_joined">Joined</string>
+    <string name="homepage_tab_source_modules">Source Modules</string>
+    <string name="homepage_tab_discover">Discover</string>
+    <string name="homepage_no_joined_modules">No joined modules</string>
+    <string name="homepage_standard_modules_sortable">Standard Modules (Drag to reorder)</string>
+    <string name="homepage_infinite_module_bottom">Bottom Infinite Module</string>
+    <string name="homepage_in_effect">In effect</string>
+    <string name="homepage_blocked">Blocked</string>
+    <string name="homepage_source_json_empty">The source\'s homepageModules JSON is empty</string>
+    <string name="homepage_already_joined">Already joined</string>
+    <string name="homepage_module_type">Module Type</string>
+    <string name="homepage_source_no_discover">This source has no discover items</string>
+    <string name="homepage_select_items">Select Items</string>
+    <string name="homepage_manual_add">+ Manual Add</string>
+    <string name="homepage_n_modules">%1$d modules</string>
+    <string name="homepage_no_modules">No modules</string>
+    <string name="homepage_browse_to_add">Browse Source Modules to Add</string>
+    <string name="homepage_standard_module">Standard Module</string>
+    <string name="homepage_infinite_module_slot">Infinite Module Slot</string>
+    <string name="homepage_new_custom_set">+ New Custom Set</string>
+    <string name="homepage_rename_custom_set">Rename Custom Set</string>
+    <string name="homepage_name_label">Name</string>
+    <string name="homepage_new_custom_set_title">New Custom Set</string>
+    <string name="homepage_delete_custom_set">Delete Custom Set</string>
+    <string name="homepage_delete_custom_set_confirm">Are you sure you want to delete this set and all its module copies?</string>
+    <string name="homepage_remove_module">Remove Module</string>
+    <string name="homepage_remove_module_confirm">Are you sure you want to remove this module from the current set?</string>
+    <string name="homepage_add_button_group">Add Button Group</string>
+    <string name="homepage_module_title_label">Module Title</string>
+    <string name="homepage_custom_title">Custom Title</string>
+    <string name="homepage_title_label">Title</string>
+    <string name="homepage_edit_module">Edit Module</string>
+    <string name="homepage_type_label">Type</string>
+    <string name="homepage_layout_config_label">Layout Config</string>
+    <string name="homepage_edit_raw_json">Edit Raw JSON (LayoutConfig)</string>
+    <string name="homepage_layout_settings">Layout Settings</string>
+    <string name="homepage_layout_mode">Homepage Layout Mode</string>
+    <string name="homepage_layout_mixed">Mixed List</string>
+    <string name="homepage_layout_tabs">Tabs by Source</string>
+    <string name="homepage_filter_sources">Filter Sources</string>
+    <string name="homepage_no_source_sets_selected">No source sets selected</string>
+    <string name="homepage_module_error">Module Error</string>
+    <string name="homepage_add_module_definition">Please add homepage module definitions in the book source</string>
+    <string name="homepage_collapse">Collapse</string>
+    <string name="homepage_show_all">Show All</string>
+    <string name="homepage_default">Default</string>
+    <string name="remove">Remove</string>
+    <string name="homepage_status_in_effect"> · In effect</string>
+    <string name="homepage_status_blocked"> · Blocked</string>
+    <string name="homepage_status_joined"> · Already joined</string>
 </resources>