This commit is contained in:
HapeLee authored and Kudomaga committed 2026-05-22 02:49:29 +08:00
1 parent b00944b612
commit bbec36bf36
19 files changed
+532 -371

No files matched your search

+1 -2
View File
@@ -101,8 +101,7 @@
"Read(//c/Users/**)", "Read(//c/Users/**)",
"Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)", "Bash(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\" -Directory -Depth 0)",
"Bash(Select-Object Name)", "Bash(Select-Object Name)",
"PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })", "PowerShell(Get-ChildItem -Path \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\" -Directory -Depth 1 | ForEach-Object { $_.FullName.Replace\\(\"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\app\\\\src\\\\main\\\\java\\\\io\\\\legado\\\\app\\\\\", \"\"\\) })"
"Bash(powershell *)"
] ]
} }
} }
@@ -1,240 +1,115 @@
# 首页模块 (homepageModules) # 首页模块 (Homepage Modules) 配置规范
在书源编辑页的「发现」Tab 中,可以配置 `首页模块 (JSON)` 字段,声明该书源为首页提供哪些内容模块。 书源的 `homepageModules` 字段允许开发者声明该书源在首页展示的内容模块。这些模块通过 JSON
数组进行定义,支持高度自定义的布局和数据来源。
## 概述 ---
- 每个模块代表首页上的一个内容区块(如轮播图、排行榜、网格书架等) ## 1. 数据结构 (Data Structure)
- 一个书源可以声明多个模块(例如同时提供"热门推荐"Banner 和"周榜"排行榜)
- 用户在首页可以自由拖拽排序、隐藏/显示各个模块
- 模块内容来自书源的**发现(explore)**接口,通过 `kindTitle` 匹配分类URL
## 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 ```json
[ [
{ {
"key": "模块唯一标识", "key": "top_banner",
"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",
"type": "banner", "type": "banner",
"title": "热门推荐", "title": "精品强推",
"kindTitle": "热门", "kindTitle": "首页推荐"
"displayCount": 6
}, },
{ {
"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", "type": "ranking",
"title": "周榜", "title": "热门榜单",
"kindTitle": "周排行", "kindTitle": "排行榜",
"displayCount": 10 "layoutConfig": {
"rows": 5
}
}, },
{ {
"key": "rank_month", "key": "explore_waterfall",
"type": "ranking", "type": "waterfall",
"title": "月榜", "title": "发现更多",
"kindTitle": "月排行", "kindTitle": "全部",
"displayCount": 10 "layoutConfig": {
}, "columns": 2
{ }
"key": "scifi",
"type": "grid",
"title": "科幻精选",
"kindTitle": "科幻",
"displayCount": 6
},
{
"key": "new_book",
"type": "card",
"title": "新书上架",
"kindTitle": "新书",
"displayCount": 8
} }
] ]
``` ```
每个 `key` 必须唯一,系统会按 JSON 数组中的声明顺序创建模块标识。
## 用户体验
- **发现模块**:首页会自动出现配置了 `homepageModules` 的已启用书源所声明的模块
- **拖拽排序**:长按模块标题旁的拖拽手柄可调整顺序
- **隐藏模块**:编辑模式下可隐藏不需要的模块
- **下拉刷新**:刷新后所有模块重新加载数据
- **点击书籍**:跳转到书籍详情页
- **点击模块标题**:跳转到该书源的完整发现页
## 注意事项
- JSON 格式必须**严格有效**,建议使用在线 JSON 校验工具检查
- 如果 JSON 解析失败,该书源的所有模块将被静默跳过
- 模块使用的图片从书籍封面(`coverUrl`)获取,请确保发现规则正确提取了封面
- `homepageModules` 为空的旧书源不会报错,也不会有首页模块展示
- 同一书源可配置多个同类型模块(如多个排行榜),只需 `key` 不同即可
- 模块标识全局唯一 ID 格式为 `"{setId}::{书源URL}::{key}"`,其中 `setId` 用于区分模块所属的集(书源集或自定义集)。
@@ -25,6 +25,11 @@ interface HomepageCustomSetDao {
@Query("UPDATE homepage_custom_sets SET sortOrder = :order WHERE id = :id") @Query("UPDATE homepage_custom_sets SET sortOrder = :order WHERE id = :id")
suspend fun setSortOrder(id: String, order: Int) 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") @Query("DELETE FROM homepage_custom_sets WHERE id = :id")
suspend fun delete(id: String) suspend fun delete(id: String)
} }
@@ -28,6 +28,11 @@ interface HomepageModuleDao {
@Query("UPDATE homepage_modules SET sortOrder = :order WHERE id = :id") @Query("UPDATE homepage_modules SET sortOrder = :order WHERE id = :id")
suspend fun setSortOrder(id: String, order: Int) 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") @Query("UPDATE homepage_modules SET customSetTitle = :title WHERE id = :id")
suspend fun setCustomSetTitle(id: String, title: String?) suspend fun setCustomSetTitle(id: String, title: String?)
@@ -34,6 +34,8 @@ class HomepageModulesRepository(
moduleDao.setEnabled(id, enabled) moduleDao.setEnabled(id, enabled)
override suspend fun setSortOrder(id: String, order: Int) = moduleDao.setSortOrder(id, order) 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?) = override suspend fun setCustomSetId(id: String, setId: String?) =
moduleDao.setCustomSetId(id, setId) moduleDao.setCustomSetId(id, setId)
@@ -55,6 +57,8 @@ class HomepageModulesRepository(
override suspend fun setCustomSetSortOrder(id: String, order: Int) = override suspend fun setCustomSetSortOrder(id: String, order: Int) =
customSetDao.setSortOrder(id, order) customSetDao.setSortOrder(id, order)
override suspend fun batchSetCustomSetSortOrders(orders: Map<String, Int>) =
customSetDao.batchSetSortOrders(orders)
override suspend fun createCustomSet(name: String): CustomSetItem { override suspend fun createCustomSet(name: String): CustomSetItem {
val entity = HomepageCustomSet( val entity = HomepageCustomSet(
@@ -15,6 +15,7 @@ interface HomepageModulesGateway {
suspend fun upsertAll(modules: List<ModuleItem>) suspend fun upsertAll(modules: List<ModuleItem>)
suspend fun setEnabled(id: String, enabled: Boolean) suspend fun setEnabled(id: String, enabled: Boolean)
suspend fun setSortOrder(id: String, order: Int) suspend fun setSortOrder(id: String, order: Int)
suspend fun batchSetSortOrders(orders: Map<String, Int>)
suspend fun setCustomSetId(id: String, setId: String?) suspend fun setCustomSetId(id: String, setId: String?)
suspend fun setCustomSetTitle(id: String, title: String?) suspend fun setCustomSetTitle(id: String, title: String?)
suspend fun delete(id: String) suspend fun delete(id: String)
@@ -27,6 +28,7 @@ interface HomepageModulesGateway {
// Custom set mutations // Custom set mutations
suspend fun upsertCustomSet(set: CustomSetItem) suspend fun upsertCustomSet(set: CustomSetItem)
suspend fun setCustomSetSortOrder(id: String, order: Int) suspend fun setCustomSetSortOrder(id: String, order: Int)
suspend fun batchSetCustomSetSortOrders(orders: Map<String, Int>)
suspend fun createCustomSet(name: String): CustomSetItem suspend fun createCustomSet(name: String): CustomSetItem
suspend fun renameCustomSet(id: String, name: String) suspend fun renameCustomSet(id: String, name: String)
suspend fun deleteCustomSet(id: String) suspend fun deleteCustomSet(id: String)
@@ -33,8 +33,9 @@ class AddBookUseCase(
if (source == null) { if (source == null) {
for (bookSourcePart in hasBookUrlPattern) { for (bookSourcePart in hasBookUrlPattern) {
try { try {
val bs = bookSourcePart.getBookSource()!! val bs = bookSourcePart.getBookSource() ?: continue
if (bookUrl.matches(bs.bookUrlPattern!!.toRegex())) { val pattern = bs.bookUrlPattern ?: continue
if (bookUrl.matches(pattern.toRegex())) {
source = bs source = bs
break break
} }
@@ -50,17 +51,16 @@ class AddBookUseCase(
) )
kotlin.runCatching { kotlin.runCatching {
WebBook.getBookInfoAwait(bookSource, book) val bookInfo = WebBook.getBookInfoAwait(bookSource, book)
}.onSuccess { val dbBook = bookRepository.getBook(bookInfo.name, bookInfo.author)
val dbBook = bookRepository.getBook(it.name, it.author)
if (dbBook != null) { if (dbBook != null) {
val toc = WebBook.getChapterListAwait(bookSource, it).getOrThrow() val toc = WebBook.getChapterListAwait(bookSource, bookInfo).getOrThrow()
dbBook.migrateTo(it, toc) dbBook.migrateTo(bookInfo, toc)
bookRepository.insert(it) bookRepository.insert(bookInfo)
bookRepository.insertChapters(*toc.toTypedArray()) bookRepository.insertChapters(*toc.toTypedArray())
} else { } else {
it.order = bookRepository.getMinOrder() - 1 bookInfo.order = bookRepository.getMinOrder() - 1
bookRepository.insert(it) bookRepository.insert(bookInfo)
} }
successCount++ successCount++
onProgress(successCount) onProgress(successCount)
@@ -5,7 +5,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
@@ -34,13 +36,16 @@ fun <T> HomepageLayoutSheet(
AppModalBottomSheet( AppModalBottomSheet(
data = data, data = data,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = "布局设置", title = stringResource(R.string.homepage_layout_settings),
) { ) {
Column { Column {
DropdownListSettingItem( DropdownListSettingItem(
title = "首页布局模式", title = stringResource(R.string.homepage_layout_mode),
selectedValue = layoutMode.toString(), selectedValue = layoutMode.toString(),
displayEntries = arrayOf("混合列表", "分源Tab"), displayEntries = arrayOf(
stringResource(R.string.homepage_layout_mixed),
stringResource(R.string.homepage_layout_tabs)
),
entryValues = arrayOf("0", "1"), entryValues = arrayOf("0", "1"),
onValueChange = { onLayoutModeChange(it.toInt()) } onValueChange = { onLayoutModeChange(it.toInt()) }
) )
@@ -21,8 +21,6 @@ import androidx.compose.material.icons.filled.DriveFileRenameOutline
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -32,14 +30,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.gson.JsonParser import com.google.gson.JsonParser
import io.legado.app.R
import io.legado.app.domain.model.HomepageModuleType import io.legado.app.domain.model.HomepageModuleType
import io.legado.app.domain.model.ModuleDef import io.legado.app.domain.model.ModuleDef
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.JsonConfigEditor import io.legado.app.ui.widget.components.JsonConfigEditor
import io.legado.app.ui.widget.components.JsonKeyEditorConfig
import io.legado.app.ui.widget.components.JsonRawEditor import io.legado.app.ui.widget.components.JsonRawEditor
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SecondaryButton
@@ -104,7 +103,8 @@ fun <T> HomepageModuleManageSheet(
var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) } var selectedKindTitles by remember(data != null) { mutableStateOf<Set<String>>(emptySet()) }
var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) } var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) }
var showAddButtonGroupDialog 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) { val currentTargetSetId = remember(selectingSetUrl) {
selectingSetUrl?.let { HomepageViewModel.customSetIdFromUrl(it) } selectingSetUrl?.let { HomepageViewModel.customSetIdFromUrl(it) }
@@ -130,15 +130,17 @@ fun <T> HomepageModuleManageSheet(
groupFilter = null groupFilter = null
}, },
title = when { title = when {
showCustomSetAddModules -> "添加模块" showCustomSetAddModules -> stringResource(R.string.homepage_add_module)
browsingSourceUrl != null && browsingDetail -> 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!!) -> 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 = { startAction = {
if (showCustomSetAddModules) { if (showCustomSetAddModules) {
@@ -179,7 +181,7 @@ fun <T> HomepageModuleManageSheet(
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false }) { onDismissRequest = { expanded = false }) {
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = "全部分组", text = stringResource(R.string.homepage_all_groups),
onClick = { groupFilter = null; expanded = false }, onClick = { groupFilter = null; expanded = false },
trailingIcon = if (groupFilter == null) { trailingIcon = if (groupFilter == null) {
{ AppIcon(Icons.Default.Check, null, Modifier.size(18.dp)) } { AppIcon(Icons.Default.Check, null, Modifier.size(18.dp)) }
@@ -221,7 +223,11 @@ fun <T> HomepageModuleManageSheet(
Column { Column {
AppTabRow( 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, selectedTabIndex = browseTab,
onTabSelected = { browseTab = it } onTabSelected = { browseTab = it }
) )
@@ -234,7 +240,7 @@ fun <T> HomepageModuleManageSheet(
.padding(24.dp), .padding(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AppText("暂无已加入的模块") AppText(stringResource(R.string.homepage_no_joined_modules))
} }
} else { } else {
var listData by remember(displaySetUrl) { var listData by remember(displaySetUrl) {
@@ -271,9 +277,9 @@ fun <T> HomepageModuleManageSheet(
) { ) {
item(key = "header_standard") { item(key = "header_standard") {
AppText( AppText(
text = "标准模块 (可拖拽排序)", text = stringResource(R.string.homepage_standard_modules_sortable),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary, color = LegadoTheme.colorScheme.primary,
modifier = Modifier.padding( modifier = Modifier.padding(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 4.dp vertical = 4.dp
@@ -318,9 +324,8 @@ fun <T> HomepageModuleManageSheet(
) )
) )
AppText( AppText(
text = "底栏无限模块", text = stringResource(R.string.homepage_infinite_module_bottom),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding( modifier = Modifier.padding(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 4.dp vertical = 4.dp
@@ -333,7 +338,9 @@ fun <T> HomepageModuleManageSheet(
infiniteModules.firstOrNull() == module infiniteModules.firstOrNull() == module
SelectionItemCard( SelectionItemCard(
title = module.title, 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, isEnabled = module.isVisible,
containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent, containerColor = if (isEffective) LegadoTheme.colorScheme.surfaceContainerHigh else LegadoTheme.colorScheme.onSheetContent,
onEnabledChange = { onToggleModule(module.id, it) }, onEnabledChange = { onToggleModule(module.id, it) },
@@ -363,7 +370,7 @@ fun <T> HomepageModuleManageSheet(
.padding(24.dp), .padding(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AppText("该书源的 homepageModules JSON 为空") AppText(stringResource(R.string.homepage_source_json_empty))
} }
} else { } else {
LazyColumn( LazyColumn(
@@ -376,7 +383,9 @@ fun <T> HomepageModuleManageSheet(
val isJoined = joinedKeys.contains(module.moduleKey) val isJoined = joinedKeys.contains(module.moduleKey)
SelectionItemCard( SelectionItemCard(
title = module.title, 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, containerColor = LegadoTheme.colorScheme.onSheetContent,
isSelected = isJoined, isSelected = isJoined,
inSelectionMode = true, inSelectionMode = true,
@@ -405,7 +414,7 @@ fun <T> HomepageModuleManageSheet(
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
} }
CompactDropdownSettingItem( CompactDropdownSettingItem(
title = "模块类型", title = stringResource(R.string.homepage_module_type),
selectedValue = browseModuleType, selectedValue = browseModuleType,
displayEntries = typeList.map { it.title }.toTypedArray(), displayEntries = typeList.map { it.title }.toTypedArray(),
entryValues = typeList.map { it.key }.toTypedArray(), entryValues = typeList.map { it.key }.toTypedArray(),
@@ -421,14 +430,14 @@ fun <T> HomepageModuleManageSheet(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AppText( AppText(
"该书源暂无发现项", stringResource(R.string.homepage_source_no_discover),
color = MaterialTheme.colorScheme.onSurfaceVariant color = LegadoTheme.colorScheme.onSurfaceVariant
) )
} }
} else { } else {
AppText( AppText(
"选择项", stringResource(R.string.homepage_select_items),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding( modifier = Modifier.padding(
horizontal = 16.dp, horizontal = 16.dp,
vertical = 4.dp vertical = 4.dp
@@ -462,7 +471,9 @@ fun <T> HomepageModuleManageSheet(
val isJoined = joinedKeys.contains(kindTitle) val isJoined = joinedKeys.contains(kindTitle)
SelectionItemCard( SelectionItemCard(
title = kindTitle, 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, containerColor = LegadoTheme.colorScheme.onSheetContent,
isSelected = isJoined, isSelected = isJoined,
inSelectionMode = true, inSelectionMode = true,
@@ -482,7 +493,7 @@ fun <T> HomepageModuleManageSheet(
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
SecondaryButton( SecondaryButton(
text = "+ 手动添加", text = stringResource(R.string.homepage_manual_add),
onClick = { onClick = {
addDialogPrefill = AddDialogPrefill(type = browseModuleType) addDialogPrefill = AddDialogPrefill(type = browseModuleType)
}, },
@@ -511,7 +522,7 @@ fun <T> HomepageModuleManageSheet(
item(key = "header_$sourceUrl") { item(key = "header_$sourceUrl") {
AppText( AppText(
text = onGetSourceName(sourceUrl), text = onGetSourceName(sourceUrl),
style = MaterialTheme.typography.labelLarge, style = LegadoTheme.typography.labelLarge,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
) )
} }
@@ -551,7 +562,7 @@ fun <T> HomepageModuleManageSheet(
val moduleCount = onGetSourceModules(source.sourceUrl, null).size val moduleCount = onGetSourceModules(source.sourceUrl, null).size
SelectionItemCard( SelectionItemCard(
title = source.sourceName, title = source.sourceName,
subtitle = "$moduleCount 个模块", subtitle = stringResource(R.string.homepage_n_modules, moduleCount),
containerColor = LegadoTheme.colorScheme.onSheetContent, containerColor = LegadoTheme.colorScheme.onSheetContent,
onToggleSelection = { onToggleSelection = {
browsingSourceUrl = source.sourceUrl browsingSourceUrl = source.sourceUrl
@@ -580,9 +591,9 @@ fun <T> HomepageModuleManageSheet(
.padding(24.dp), .padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
AppText("暂无模块") AppText(stringResource(R.string.homepage_no_modules))
SecondaryButton( SecondaryButton(
text = "浏览书源模块添加", text = stringResource(R.string.homepage_browse_to_add),
onClick = { onClick = {
if (setId.startsWith("src_")) { if (setId.startsWith("src_")) {
browsingSourceUrl = setId.removePrefix("src_") browsingSourceUrl = setId.removePrefix("src_")
@@ -623,8 +634,8 @@ fun <T> HomepageModuleManageSheet(
if (listData.isNotEmpty()) { if (listData.isNotEmpty()) {
item(key = "header_std_detail") { item(key = "header_std_detail") {
AppText( AppText(
text = "标准模块", text = stringResource(R.string.homepage_standard_module),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
) )
} }
@@ -659,16 +670,10 @@ fun <T> HomepageModuleManageSheet(
if (infiniteModules.isNotEmpty()) { if (infiniteModules.isNotEmpty()) {
item(key = "header_inf_detail") { item(key = "header_inf_detail") {
HorizontalDivider( PillDivider()
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 16.dp
)
)
AppText( AppText(
text = "无限模块槽位", text = stringResource(R.string.homepage_infinite_module_slot),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
) )
} }
@@ -697,7 +702,7 @@ fun <T> HomepageModuleManageSheet(
item(key = "browse_from_set") { item(key = "browse_from_set") {
SecondaryButton( SecondaryButton(
text = "浏览书源模块", text = stringResource(R.string.homepage_browse_source_modules),
onClick = { onClick = {
if (setId.startsWith("src_")) { if (setId.startsWith("src_")) {
browsingSourceUrl = setId.removePrefix("src_") browsingSourceUrl = setId.removePrefix("src_")
@@ -746,7 +751,7 @@ fun <T> HomepageModuleManageSheet(
state = setsReorderableState, state = setsReorderableState,
key = set.sourceUrl, key = set.sourceUrl,
title = set.sourceName, title = set.sourceName,
subtitle = "${set.moduleCount} 个模块", subtitle = stringResource(R.string.homepage_n_modules, set.moduleCount),
containerColor = LegadoTheme.colorScheme.onSheetContent, containerColor = LegadoTheme.colorScheme.onSheetContent,
isEnabled = set.isSelected, isEnabled = set.isSelected,
onToggleSelection = { selectingSetUrl = set.sourceUrl }, onToggleSelection = { selectingSetUrl = set.sourceUrl },
@@ -771,14 +776,14 @@ fun <T> HomepageModuleManageSheet(
} }
item(key = "create_set") { item(key = "create_set") {
SecondaryButton( SecondaryButton(
text = "+ 新建自定义集", text = stringResource(R.string.homepage_new_custom_set),
onClick = { showCreateSetDialog = true }, onClick = { showCreateSetDialog = true },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
item(key = "browse_sources") { item(key = "browse_sources") {
SecondaryButton( SecondaryButton(
text = "浏览书源模块", text = stringResource(R.string.homepage_browse_source_modules),
onClick = { showSourceBrowser = true }, onClick = { showSourceBrowser = true },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
@@ -792,7 +797,7 @@ fun <T> HomepageModuleManageSheet(
AppAlertDialog( AppAlertDialog(
data = renameSetId, data = renameSetId,
onDismissRequest = { renameSetId = null }, onDismissRequest = { renameSetId = null },
title = "重命名自定义集", title = stringResource(R.string.homepage_rename_custom_set),
content = { setId -> content = { setId ->
val currentName = val currentName =
remember(setId) { sets.find { it.sourceUrl == setId }?.sourceName ?: "" } remember(setId) { sets.find { it.sourceUrl == setId }?.sourceName ?: "" }
@@ -800,7 +805,7 @@ fun <T> HomepageModuleManageSheet(
AppTextField( AppTextField(
value = tempName, value = tempName,
onValueChange = { tempName = it }, onValueChange = { tempName = it },
label = "名称", label = stringResource(R.string.homepage_name_label),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}, },
@@ -811,21 +816,21 @@ fun <T> HomepageModuleManageSheet(
) )
renameSetId = null renameSetId = null
}, },
confirmText = "确定", confirmText = stringResource(R.string.dialog_confirm),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { renameSetId = null } onDismiss = { renameSetId = null }
) )
AppAlertDialog( AppAlertDialog(
data = if (showCreateSetDialog) Unit else null, data = if (showCreateSetDialog) Unit else null,
onDismissRequest = { showCreateSetDialog = false }, onDismissRequest = { showCreateSetDialog = false },
title = "新建自定义集", title = stringResource(R.string.homepage_new_custom_set_title),
content = { content = {
LaunchedEffect(Unit) { tempName = "" } LaunchedEffect(Unit) { tempName = "" }
AppTextField( AppTextField(
value = tempName, value = tempName,
onValueChange = { tempName = it }, onValueChange = { tempName = it },
label = "名称", label = stringResource(R.string.homepage_name_label),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}, },
@@ -833,36 +838,36 @@ fun <T> HomepageModuleManageSheet(
if (tempName.isNotBlank()) onCreateCustomSet(tempName) if (tempName.isNotBlank()) onCreateCustomSet(tempName)
showCreateSetDialog = false showCreateSetDialog = false
}, },
confirmText = "确定", confirmText = stringResource(R.string.dialog_confirm),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { showCreateSetDialog = false } onDismiss = { showCreateSetDialog = false }
) )
AppAlertDialog( AppAlertDialog(
data = deleteSetConfirmId, data = deleteSetConfirmId,
onDismissRequest = { deleteSetConfirmId = null }, onDismissRequest = { deleteSetConfirmId = null },
title = "删除自定义集", title = stringResource(R.string.homepage_delete_custom_set),
text = "确定要删除该集及其包含的所有模块副本吗?", text = stringResource(R.string.homepage_delete_custom_set_confirm),
onConfirm = { setId -> onConfirm = { setId ->
onDeleteCustomSet(HomepageViewModel.customSetIdFromUrl(setId)) onDeleteCustomSet(HomepageViewModel.customSetIdFromUrl(setId))
deleteSetConfirmId = null deleteSetConfirmId = null
}, },
confirmText = "删除", confirmText = stringResource(R.string.delete),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { deleteSetConfirmId = null } onDismiss = { deleteSetConfirmId = null }
) )
AppAlertDialog( AppAlertDialog(
data = deleteConfirmId, data = deleteConfirmId,
onDismissRequest = { deleteConfirmId = null }, onDismissRequest = { deleteConfirmId = null },
title = "移除模块", title = stringResource(R.string.homepage_remove_module),
text = "确定要从当前集中移除该模块吗?", text = stringResource(R.string.homepage_remove_module_confirm),
onConfirm = { id -> onConfirm = { id ->
onDeleteModule(id) onDeleteModule(id)
deleteConfirmId = null deleteConfirmId = null
}, },
confirmText = "移除", confirmText = stringResource(R.string.remove),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { deleteConfirmId = null } onDismiss = { deleteConfirmId = null }
) )
@@ -900,13 +905,14 @@ fun <T> HomepageModuleManageSheet(
AppAlertDialog( AppAlertDialog(
data = if (showAddButtonGroupDialog) Unit else null, data = if (showAddButtonGroupDialog) Unit else null,
onDismissRequest = { showAddButtonGroupDialog = false }, onDismissRequest = { showAddButtonGroupDialog = false },
title = "添加按钮组", title = stringResource(R.string.homepage_add_button_group),
content = { content = {
LaunchedEffect(Unit) { tempButtonGroupTitle = "快捷操作" } val quickActionsLabel = stringResource(R.string.homepage_quick_actions)
LaunchedEffect(Unit) { tempButtonGroupTitle = quickActionsLabel }
AppTextField( AppTextField(
value = tempButtonGroupTitle, value = tempButtonGroupTitle,
onValueChange = { tempButtonGroupTitle = it }, onValueChange = { tempButtonGroupTitle = it },
label = "模块标题", label = stringResource(R.string.homepage_module_title_label),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}, },
@@ -920,21 +926,21 @@ fun <T> HomepageModuleManageSheet(
selectedKindTitles = emptySet() selectedKindTitles = emptySet()
showAddButtonGroupDialog = false showAddButtonGroupDialog = false
}, },
confirmText = "确定", confirmText = stringResource(R.string.dialog_confirm),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { showAddButtonGroupDialog = false } onDismiss = { showAddButtonGroupDialog = false }
) )
AppAlertDialog( AppAlertDialog(
data = customSetTitleEdit, data = customSetTitleEdit,
onDismissRequest = { customSetTitleEdit = null }, onDismissRequest = { customSetTitleEdit = null },
title = "自定义标题", title = stringResource(R.string.homepage_custom_title),
content = { (_, title) -> content = { (_, title) ->
LaunchedEffect(title) { titleState = title } LaunchedEffect(title) { titleState = title }
AppTextField( AppTextField(
value = titleState, value = titleState,
onValueChange = { titleState = it }, onValueChange = { titleState = it },
label = "标题", label = stringResource(R.string.homepage_title_label),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
}, },
@@ -942,8 +948,8 @@ fun <T> HomepageModuleManageSheet(
onSetCustomSetTitle(id, titleState.takeIf { it.isNotBlank() }) onSetCustomSetTitle(id, titleState.takeIf { it.isNotBlank() })
customSetTitleEdit = null customSetTitleEdit = null
}, },
confirmText = "确定", confirmText = stringResource(R.string.dialog_confirm),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = { customSetTitleEdit = null } onDismiss = { customSetTitleEdit = null }
) )
} }
@@ -974,24 +980,11 @@ fun <T> AddCustomModuleDialog(
var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) } var layoutConfig by remember(data) { mutableStateOf(prefillLayoutConfig) }
var showRawLayoutConfig by remember(data) { mutableStateOf(false) } 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) { val hasVisualizableKeys = remember(layoutConfig) {
runCatching { runCatching {
val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject val jsonObject = JsonParser.parseString(layoutConfig).asJsonObject
jsonObject.keySet().any { key -> jsonObject.keySet().any { key ->
key == "columns" || key == "rows" || layoutKeyConfigs.containsKey(key) key == "columns" || key == "rows"
} }
}.getOrElse { false } }.getOrElse { false }
} }
@@ -999,7 +992,9 @@ fun <T> AddCustomModuleDialog(
AppAlertDialog( AppAlertDialog(
data = data, data = data,
onDismissRequest = onDismissRequest, 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 = { content = {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -1011,7 +1006,7 @@ fun <T> AddCustomModuleDialog(
AppTextField( AppTextField(
value = title, value = title,
onValueChange = { title = it }, onValueChange = { title = it },
label = "标题", label = stringResource(R.string.homepage_title_label),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
AppTextField( AppTextField(
@@ -1024,7 +1019,7 @@ fun <T> AddCustomModuleDialog(
HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown }
} }
DropdownListSettingItem( DropdownListSettingItem(
title = "类型", title = stringResource(R.string.homepage_type_label),
selectedValue = type, selectedValue = type,
displayEntries = typeList.map { it.title }.toTypedArray(), displayEntries = typeList.map { it.title }.toTypedArray(),
entryValues = typeList.map { it.key }.toTypedArray(), entryValues = typeList.map { it.key }.toTypedArray(),
@@ -1037,20 +1032,18 @@ fun <T> AddCustomModuleDialog(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
AppText( AppText(
text = "布局配置", text = stringResource(R.string.homepage_layout_config_label),
style = MaterialTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) modifier = Modifier.padding(top = 16.dp, bottom = 4.dp)
) )
if (hasVisualizableKeys) { if (hasVisualizableKeys) {
JsonConfigEditor( JsonConfigEditor(
jsonString = layoutConfig, jsonString = layoutConfig,
onJsonStringChange = { layoutConfig = it }, onJsonStringChange = { layoutConfig = it },
keyConfigs = layoutKeyConfigs,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
CompactClickableSettingItem( CompactClickableSettingItem(
title = "编辑原始 JSON (LayoutConfig)", title = stringResource(R.string.homepage_edit_raw_json),
onClick = { showRawLayoutConfig = !showRawLayoutConfig } onClick = { showRawLayoutConfig = !showRawLayoutConfig }
) )
if (showRawLayoutConfig) { if (showRawLayoutConfig) {
@@ -1082,8 +1075,8 @@ fun <T> AddCustomModuleDialog(
) )
) )
}, },
confirmText = "确定", confirmText = stringResource(R.string.dialog_confirm),
dismissText = "取消", dismissText = stringResource(R.string.dialog_cancel),
onDismiss = onDismissRequest onDismiss = onDismissRequest
) )
} }
@@ -44,11 +44,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R
import io.legado.app.domain.model.HomepageModuleType import io.legado.app.domain.model.HomepageModuleType
import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.main.bookCoverSharedElementKey
import io.legado.app.ui.main.homepage.modules.BannerModule import io.legado.app.ui.main.homepage.modules.BannerModule
@@ -102,6 +104,7 @@ fun HomepageScreen(
}) })
val mixedGridState = rememberLazyStaggeredGridState() val mixedGridState = rememberLazyStaggeredGridState()
val homeString = stringResource(R.string.home)
val currentTitle by remember( val currentTitle by remember(
layoutMode, layoutMode,
pagerState.currentPage, pagerState.currentPage,
@@ -110,16 +113,16 @@ fun HomepageScreen(
) { ) {
derivedStateOf { derivedStateOf {
if (layoutMode == 1) { if (layoutMode == 1) {
"首页" homeString
} else { } else {
val firstHeader = mixedGridState.layoutInfo.visibleItemsInfo.firstOrNull { val firstHeader = mixedGridState.layoutInfo.visibleItemsInfo.firstOrNull {
(it.key as? String)?.startsWith("header_") == true (it.key as? String)?.startsWith("header_") == true
} }
if (firstHeader != null) { if (firstHeader != null) {
val id = (firstHeader.key as? String).orEmpty().substringAfter("header_", "") 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 { } else {
"首页" homeString
} }
} }
} }
@@ -198,7 +201,7 @@ fun HomepageScreen(
} else { } else {
if (selectedSets.isEmpty()) { if (selectedSets.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
AppText("未选择任何书源集") AppText(stringResource(R.string.homepage_no_source_sets_selected))
} }
} else { } else {
HorizontalPager( HorizontalPager(
@@ -234,13 +237,13 @@ fun HomepageScreen(
AppAlertDialog( AppAlertDialog(
data = errorMsg, data = errorMsg,
onDismissRequest = { errorMsg = null }, onDismissRequest = { errorMsg = null },
title = "模块错误", title = stringResource(R.string.homepage_module_error),
confirmText = "复制", confirmText = stringResource(R.string.copy_text),
onConfirm = { onConfirm = {
context.sendToClip(it) context.sendToClip(it)
errorMsg = null errorMsg = null
}, },
dismissText = "关闭", dismissText = stringResource(R.string.close),
onDismiss = { errorMsg = null } onDismiss = { errorMsg = null }
) )
@@ -316,7 +319,7 @@ private fun ModuleList(
) { ) {
if (modules.isEmpty()) { if (modules.isEmpty()) {
Box(modifier = modifier, contentAlignment = Alignment.Center) { Box(modifier = modifier, contentAlignment = Alignment.Center) {
AppText("请在书源中添加首页模块定义") AppText(stringResource(R.string.homepage_add_module_definition))
} }
} else { } else {
// 1. 过滤和重排模块:每个集只能有一个无限流模块,且必须在最下面 // 1. 过滤和重排模块:每个集只能有一个无限流模块,且必须在最下面
@@ -403,7 +406,7 @@ private fun ModuleList(
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
SecondaryButton( SecondaryButton(
text = "重试", text = stringResource(R.string.retry),
onClick = { onClick = {
viewModel.retryModule(moduleUi.globalId) viewModel.retryModule(moduleUi.globalId)
} }
@@ -45,7 +45,7 @@ fun HomepageSourceSelectSheet(
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = "筛选书源", title = stringResource(R.string.homepage_filter_sources),
) { ) {
Column { Column {
SearchBar( SearchBar(
@@ -2,6 +2,7 @@ package io.legado.app.ui.main.homepage
import android.app.Application import android.app.Application
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.R
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
@@ -90,6 +91,7 @@ class HomepageViewModel(
private val localModulesFlow = gateway.flowEnabled() private val localModulesFlow = gateway.flowEnabled()
private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap()) private val _bookSourcesCache = MutableStateFlow<Map<String, BookSource>>(emptyMap())
private val _layoutConfigCache = MutableStateFlow<Map<String, Map<String, String>>>(emptyMap())
val allModulesCache = gateway.flowAll() val allModulesCache = gateway.flowAll()
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
@@ -115,8 +117,8 @@ class HomepageViewModel(
) { grouped, contentStates, flags, sourcesCache, customSets -> ) { grouped, contentStates, flags, sourcesCache, customSets ->
val setNames = customSets.associate { it.id to it.name } val setNames = customSets.associate { it.id to it.name }
val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id } val sortedSetIds = customSets.sortedBy { it.sortOrder }.map { it.id }
val configCache = _layoutConfigCache.value
// 按照集排序设置来排布模块
val displayModules = sortedSetIds.flatMap { setId -> val displayModules = sortedSetIds.flatMap { setId ->
val setUrl = customSetUrl(setId) val setUrl = customSetUrl(setId)
val mods = grouped[setUrl] ?: emptyList() val mods = grouped[setUrl] ?: emptyList()
@@ -125,17 +127,7 @@ class HomepageViewModel(
val sourceName = source?.bookSourceName ?: module.sourceUrl val sourceName = source?.bookSourceName ?: module.sourceUrl
val setName = module.customSetId?.let { setNames[it] } ?: sourceName val setName = module.customSetId?.let { setNames[it] } ?: sourceName
val exploreUrl = module.url ?: source?.exploreUrl val exploreUrl = module.url ?: source?.exploreUrl
val configMap = configCache[module.id] ?: emptyMap()
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) {
}
}
HomepageModuleUi( HomepageModuleUi(
sourceUrl = module.sourceUrl, sourceUrl = module.sourceUrl,
@@ -203,6 +195,26 @@ class HomepageViewModel(
private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList()) private val _pendingUserModules = MutableStateFlow<List<ModuleItem>>(emptyList())
init { 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 的书源 // sync: 只处理有 homepageModules 的书源
viewModelScope.launch { viewModelScope.launch {
initModulesSyncFlow.collect { sources -> initModulesSyncFlow.collect { sources ->
@@ -438,7 +450,14 @@ class HomepageViewModel(
states[globalId] as? ModuleLoadState.Loaded ?: return@update states states[globalId] as? ModuleLoadState.Loaded ?: return@update states
states + (globalId to lastState.copy(isLoadingMore = false)) 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) { if (hasInfinite) {
viewModelScope.launch { viewModelScope.launch {
_effects.emit(HomepageEffect.ShowSnackbar("该分组已存在无限加载模块")) _effects.emit(
HomepageEffect.ShowSnackbar(
getApplication<Application>().getString(
R.string.homepage_module_duplicate_infinite
)
)
)
} }
return return
} }
@@ -603,19 +628,18 @@ class HomepageViewModel(
fun reorderJoinedModules(orderedIds: List<String>) { fun reorderJoinedModules(orderedIds: List<String>) {
viewModelScope.launch { viewModelScope.launch {
orderedIds.forEachIndexed { index, id -> val orders = orderedIds.mapIndexed { index, id -> id to index }.toMap()
gateway.setSortOrder(id, index) gateway.batchSetSortOrders(orders)
}
notifyConfigChanged() notifyConfigChanged()
} }
} }
fun reorderCustomSets(orderedUrls: List<String>) { fun reorderCustomSets(orderedUrls: List<String>) {
viewModelScope.launch { viewModelScope.launch {
orderedUrls.forEachIndexed { index, url -> val orders = orderedUrls.mapIndexed { index, url ->
val id = customSetIdFromUrl(url) customSetIdFromUrl(url) to index
gateway.setCustomSetSortOrder(id, index) }.toMap()
} gateway.batchSetCustomSetSortOrders(orders)
notifyConfigChanged() notifyConfigChanged()
} }
} }
@@ -749,7 +773,13 @@ class HomepageViewModel(
} }
if (hasInfinite) { if (hasInfinite) {
viewModelScope.launch { viewModelScope.launch {
_effects.emit(HomepageEffect.ShowSnackbar("该分组已存在无限加载模块")) _effects.emit(
HomepageEffect.ShowSnackbar(
getApplication<Application>().getString(
R.string.homepage_module_duplicate_infinite
)
)
)
} }
return return
} }
@@ -133,7 +133,7 @@ fun ButtonGroupModule(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(vertical = 12.dp, horizontal = 4.dp) .padding(vertical = 8.dp, horizontal = 4.dp)
) { ) {
if (hasIcon) { if (hasIcon) {
SourceIcon( SourceIcon(
@@ -143,12 +143,12 @@ fun ButtonGroupModule(
} }
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(6.dp))
} }
AppText( AppText(
text = displayName, text = displayName,
style = LegadoTheme.typography.labelMedium, style = LegadoTheme.typography.labelSmallEmphasized,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Clip, overflow = TextOverflow.Clip,
@@ -22,10 +22,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.domain.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.main.bookCoverSharedElementKey
@@ -93,7 +95,9 @@ fun RankingModule(
modifier = Modifier.size(20.dp) modifier = Modifier.size(20.dp)
) )
AppText( AppText(
text = if (isExpanded) "收起" else "显示全部", text = if (isExpanded) stringResource(R.string.homepage_collapse) else stringResource(
R.string.homepage_show_all
),
style = LegadoTheme.typography.labelMediumEmphasized, style = LegadoTheme.typography.labelMediumEmphasized,
color = if (isExpanded) LegadoTheme.colorScheme.outline else LegadoTheme.colorScheme.primary, color = if (isExpanded) LegadoTheme.colorScheme.outline else LegadoTheme.colorScheme.primary,
modifier = Modifier.padding(start = 4.dp) modifier = Modifier.padding(start = 4.dp)
@@ -77,8 +77,8 @@ fun CoilBookCover(
} }
val hasCustomDefault = !randomPath.isNullOrBlank() val hasCustomDefault = !randomPath.isNullOrBlank()
var isOnlineCoverLoaded by remember(path) { var isOnlineCoverLoaded by remember(path, sharedCoverKey, finalPath) {
mutableStateOf(false) mutableStateOf(sharedCoverKey != null && finalPath != null)
} }
Box( Box(
@@ -1674,4 +1674,63 @@
<string name="load_with_web_log">显示WebView日志</string> <string name="load_with_web_log">显示WebView日志</string>
<string name="cache_first">优先缓存</string> <string name="cache_first">优先缓存</string>
<string name="refresh_limit_unlimited">无限制</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> </resources>
@@ -1312,4 +1312,63 @@
<string name="cache_first">优先缓存</string> <string name="cache_first">优先缓存</string>
<string name="refresh_limit_unlimited">无限制</string> <string name="refresh_limit_unlimited">无限制</string>
<string name="refresh_limit_books">%1$d 本</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> </resources>
@@ -1315,4 +1315,63 @@
<string name="cache_first">优先缓存</string> <string name="cache_first">优先缓存</string>
<string name="refresh_limit_unlimited">无限制</string> <string name="refresh_limit_unlimited">无限制</string>
<string name="refresh_limit_books">%1$d 本</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> </resources>
+59
View File
@@ -1680,4 +1680,63 @@
<string name="download_setting">Download</string> <string name="download_setting">Download</string>
<string name="image_cache">Image Cache</string> <string name="image_cache">Image Cache</string>
<string name="network">Network</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> </resources>