This commit is contained in:
HapeLee
2026-05-22 02:49:29 +08:00
committed by Kudomaga
parent b00944b612
commit bbec36bf36
19 changed files with 532 additions and 371 deletions
@@ -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(