优化
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user