This commit is contained in:
HapeLee
2026-05-22 02:47:40 +08:00
parent 4f2a716869
commit 7e35177bac
19 changed files with 532 additions and 371 deletions
+1 -2
View File
@@ -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\\\\\", \"\"\\) })"
]
}
}
@@ -1,240 +1,115 @@
# 首页模块 (homepageModules)
# 首页模块 (Homepage Modules) 配置规范
书源编辑页的「发现」Tab 中,可以配置 `首页模块 (JSON)` 字段,声明该书源首页提供哪些内容模块。
书源`homepageModules` 字段允许开发者声明该书源首页展示的内容模块。这些模块通过 JSON
数组进行定义,支持高度自定义的布局和数据来源。
## 概述
---
- 每个模块代表首页上的一个内容区块(如轮播图、排行榜、网格书架等)
- 一个书源可以声明多个模块(例如同时提供"热门推荐"Banner 和"周榜"排行榜)
- 用户在首页可以自由拖拽排序、隐藏/显示各个模块
- 模块内容来自书源的**发现(explore)**接口,通过 `kindTitle` 匹配分类URL
## 1. 数据结构 (Data Structure)
## JSON 格式
`homepageModules` 是一个包含多个模块定义对象的 JSON 数组。
`homepageModules` 是一个 JSON 数组,每个元素定义一个模块:
### 模块通用字段
| 字段 | 类型 | 必须 | 说明 |
|:-----------------|:---------|:---|:------------------------------------------------------|
| **key** | `String` | 是 | 模块唯一标识。建议使用 `[a-z0-9_]` 字符。用于保存用户的排序/显隐设置。 |
| **type** | `Enum` | 是 | 模块类型。定义了渲染方式和交互逻辑。详见 [模块类型](#2-模块类型-module-types)。 |
| **title** | `String` | 是 | 模块默认标题。用户可在本地自定义覆盖。 |
| **kindTitle** | `String` | 否 | 用于匹配书源「发现」规则中的分类标题。匹配成功后自动继承其 URL 和规则。 |
| **url** | `String` | 否 | 显式指定数据接口 URL。优先级高于 `kindTitle`。支持变量替换。 |
| **args** | `String` | 否 | 附加参数。在 `buttonGroup` 类型中为 JSON 数组字符串。 |
| **layoutConfig** | `Object` | 否 | 布局配置对象,用于调整列数、行数、图标等。详见 [布局配置](#3-布局配置-layoutconfig)。 |
---
## 2. 模块类型 (Module Types)
### 列表与轮播类
| 类型 (Type) | 描述 | 特点 |
|:----------|:------|:-----------------------|
| `banner` | 横滑轮播图 | 适合展示高权重的精品推荐,使用大图封面。 |
| `ranking` | 排行榜列表 | 垂直列表展示,带排名序号。 |
| `card` | 推荐卡片 | 横向滑动的卡片流,同时显示封面、标题及简介。 |
### 网格类
| 类型 (Type) | 描述 | 特点 |
|:---------------|:------|:------------------|
| `grid` | 标准网格 | 最常用的展示形式。支持自定义行列。 |
| `gridRanking` | 网格排行榜 | 多行多列的排行展示。横向翻页。 |
| `infiniteGrid` | 无限网格 | 垂直滚动的网格流。无限加载。 |
| `waterfall` | 错位瀑布流 | 垂直错位排列的书架流。无限加载。 |
### 功能类
| 类型 (Type) | 描述 | 特点 |
|:--------------|:------|:--------------------------------------------|
| `buttonGroup` | 快捷按钮组 | 渲染为一组圆形/图标按钮,支持自动填充宽度与自动分列。通常用于放置常用分类或功能入口。 |
---
## 3. 布局配置 (LayoutConfig)
通过 `layoutConfig` 对象,可以精细化控制模块的表现。
| 属性 (Property) | 类型 | 适用类型 | 默认值 | 说明 |
|:--------------|:---------|:------------------------------------|:----|:-----------------------------------------|
| `columns` | `Int` | `grid`, `waterfall`, `infiniteGrid` | 3 | 每行显示的列数。 |
| `icon` | `String` | `buttonGroup` | - | 按钮组的默认统一图标 URL。 |
| `icons` | `Object` | `buttonGroup` | - | 图标映射表。例:`{"排行": "http://path/to/icon"}`。 |
---
## 4. 数据绑定逻辑 (Data Binding)
1. **自动匹配**:如果提供了 `kindTitle`,系统会遍历书源 `exploreKinds()` 返回的列表。如果某个分类的
`title` 与之完全一致,该模块将自动使用该分类的 `url`
2. **静态指定**:如果提供了 `url`,系统将直接请求该 URL。
3. **降级逻辑**:若 `kindTitle` 未匹配且无 `url`,模块将回退至书源的主 `exploreUrl`
---
## 5. 完整 JSON 示例
```json
[
{
"key": "模块唯一标识",
"type": "模块类型",
"title": "模块标题",
"kindTitle": "匹配的分类标题(可选)",
"url": "覆盖分类URL(可选)",
"args": "特殊参数(可选)",
"layoutConfig": {
"columns": 2,
"rows": 3
}
}
]
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|----------------|-----------|----|-------------------------------------------------------------------------------------------------------|
| `key` | `String` | 是 | 模块在书源内的唯一标识,用于关联用户偏好。建议使用英文,如 `"hot"`, `"rank_week"` |
| `type` | `String` | 是 | 模块类型,可选值:`"banner"`, `"ranking"`, `"gridRanking"`, `"grid"`, `"card"`, `"waterfall"`, `"buttonGroup"` |
| `title` | `String` | 是 | 模块标题,会在首页模块头部展示 |
| `kindTitle` | `String?` | 否 | 用于匹配该书源「发现」中的分类标题。不填则使用默认 `exploreUrl` |
| `url` | `String?` | 否 | 显式指定该模块的 URL,优先级高于 `kindTitle` 匹配到的 URL |
| `args` | `String?` | 否 | 模块特有参数。在 `buttonGroup` 中为包含分类标题的 JSON 数组字符串 |
| `layoutConfig` | `Object?` | 否 | 布局配置对象。支持 `columns` (列数), `rows` (行数) |
## 模块类型
### banner — 横滑轮播图
适合展示热门推荐、本周强推等内容。以大图封面横向滑动展示。
```json
{
"key": "hot_banner",
"type": "banner",
"title": "热门推荐",
"kindTitle": "热门",
"displayCount": 6
}
```
### ranking — 排行榜
带排名序号的列表。前三名高亮为橙色,其余为灰色。默认折叠显示前 5 本,末尾有"展开更多"按钮。
```json
{
"key": "week_rank",
"type": "ranking",
"title": "周排行榜",
"kindTitle": "周榜",
"displayCount": 10
}
```
### gridRanking — 网格排行
4×4 列式排列(可通过 `layoutConfig.rows` 修改行数),每页多本书,可横向翻页。封面较小,仅显示书名和作者。适合作为首页入口展示大量书籍。
```json
{
"key": "all_grid",
"type": "gridRanking",
"title": "全部分类",
"kindTitle": "全部分类",
"layoutConfig": {
"rows": 4
}
}
```
### grid — 网格书架
网格布局,适合展示分类书单。支持自定义行列。
- `columns`: 默认 3
- `rows`: 默认 2。若设置为 `0`,则显示为平铺列表并支持下拉加载更多。
```json
{
"key": "scifi_grid",
"type": "grid",
"title": "科幻精选",
"kindTitle": "科幻",
"layoutConfig": {
"columns": 3,
"rows": 2
}
}
```
### card — 推荐卡片
大图横向滑动卡片,展示封面 + 书名 + 简介。适合需要更多信息展示的场景。
```json
{
"key": "editor_pick",
"type": "card",
"title": "编辑推荐",
"kindTitle": "编辑推荐"
}
```
### waterfall — 错位瀑布流
瀑布流布局,展示封面 + 书名 + 简介。支持 `layoutConfig.columns` 自定义列数(默认 2)。支持无限加载更多。
```json
{
"key": "hot_wf",
"type": "waterfall",
"title": "大家都在看",
"kindTitle": "热门",
"layoutConfig": {
"columns": 2
}
}
```
### buttonGroup — 按钮组
显示为网格排列的分类按钮。适合放置常用的分类或动作入口。
- **布局特性**:自动平衡每行按钮数量(例如 6 个按钮显示为 3+3,8 个显示为 4+4)。每行最多 5 个。
- **args**: 可选。由分类标题组成的 JSON 数组字符串,如 `["排行", "分类", "完本"]`。若不填则默认显示该书源前
5 个分类。
- **layoutConfig**:
- `icon`: 全局默认图标。支持网络图片 URL。
- `icons`: 图标映射对象。以分类标题为键,网络图片 URL 为值。
```json
{
"key": "entry_buttons",
"type": "buttonGroup",
"title": "快捷入口",
"args": "[\"排行\", \"分类\", \"我的\"]",
"layoutConfig": {
"icon": "https://example.com/default.png",
"icons": {
"排行": "https://example.com/rank.png",
"我的": "https://example.com/my.png"
}
}
}
```
## kindTitle 匹配规则
模块加载时,系统会根据 `kindTitle` 在书源的「发现」分类列表中进行匹配:
1.`exploreKinds()`(即发现页的分类列表)中查找 `title` 等于 `kindTitle` 的分类
2. 如果匹配成功,使用该分类的 `url` 来加载数据
3. 如果匹配失败,或 `kindTitle` 为空,则使用书源的默认 `exploreUrl`
4. 如果两个 URL 都没有,该模块将显示加载错误
**建议**:确保 `kindTitle` 的值与发现页分类的标题**完全一致**(包括大小写和标点)。
## 完整示例
以下是一个书源配置了多种模块的完整 JSON:
```json
[
{
"key": "hot",
"key": "top_banner",
"type": "banner",
"title": "热门推荐",
"kindTitle": "热门",
"displayCount": 6
"title": "精品强推",
"kindTitle": "首页推荐"
},
{
"key": "rank_week",
"key": "quick_nav",
"type": "buttonGroup",
"title": "分类导航",
"args": "[\"武侠\", \"仙侠\", \"都市\", \"历史\"]",
"layoutConfig": {
"icon": "https://example.com/icons/default.png",
"icons": {
"武侠": "https://example.com/icons/wuxia.png"
}
}
},
{
"key": "hot_rank",
"type": "ranking",
"title": "周榜",
"kindTitle": "排行",
"displayCount": 10
"title": "热门榜单",
"kindTitle": "排行",
"layoutConfig": {
"rows": 5
}
},
{
"key": "rank_month",
"type": "ranking",
"title": "月榜",
"kindTitle": "月排行",
"displayCount": 10
},
{
"key": "scifi",
"type": "grid",
"title": "科幻精选",
"kindTitle": "科幻",
"displayCount": 6
},
{
"key": "new_book",
"type": "card",
"title": "新书上架",
"kindTitle": "新书",
"displayCount": 8
"key": "explore_waterfall",
"type": "waterfall",
"title": "发现更多",
"kindTitle": "全部",
"layoutConfig": {
"columns": 2
}
}
]
```
每个 `key` 必须唯一,系统会按 JSON 数组中的声明顺序创建模块标识。
## 用户体验
- **发现模块**:首页会自动出现配置了 `homepageModules` 的已启用书源所声明的模块
- **拖拽排序**:长按模块标题旁的拖拽手柄可调整顺序
- **隐藏模块**:编辑模式下可隐藏不需要的模块
- **下拉刷新**:刷新后所有模块重新加载数据
- **点击书籍**:跳转到书籍详情页
- **点击模块标题**:跳转到该书源的完整发现页
## 注意事项
- JSON 格式必须**严格有效**,建议使用在线 JSON 校验工具检查
- 如果 JSON 解析失败,该书源的所有模块将被静默跳过
- 模块使用的图片从书籍封面(`coverUrl`)获取,请确保发现规则正确提取了封面
- `homepageModules` 为空的旧书源不会报错,也不会有首页模块展示
- 同一书源可配置多个同类型模块(如多个排行榜),只需 `key` 不同即可
- 模块标识全局唯一 ID 格式为 `"{setId}::{书源URL}::{key}"`,其中 `setId` 用于区分模块所属的集(书源集或自定义集)。
@@ -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)
}
@@ -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?)
@@ -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(
@@ -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)
@@ -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)
@@ -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()) }
)
@@ -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
)
}
@@ -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)
}
@@ -45,7 +45,7 @@ fun HomepageSourceSelectSheet(
AppModalBottomSheet(
show = show,
onDismissRequest = onDismissRequest,
title = "筛选书源",
title = stringResource(R.string.homepage_filter_sources),
) {
Column {
SearchBar(
@@ -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
}
@@ -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,
@@ -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)
@@ -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(
@@ -1532,4 +1532,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>
@@ -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>
@@ -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
View File
@@ -1538,4 +1538,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>