修复一些问题

This commit is contained in:
HapeLee
2026-05-24 02:39:59 +08:00
parent 5b59b5a743
commit 7b32cb732f
13 changed files with 194 additions and 206 deletions
@@ -274,6 +274,7 @@ object PreferKey {
const val bookshelfGroupListStyle = "bookshelfGroupListStyle" const val bookshelfGroupListStyle = "bookshelfGroupListStyle"
const val bookshelfGroupCoverCount = "bookshelfGroupCoverCount" const val bookshelfGroupCoverCount = "bookshelfGroupCoverCount"
const val bookshelfListCoverWidth = "bookshelfListCoverWidth" const val bookshelfListCoverWidth = "bookshelfListCoverWidth"
const val bookshelfGridCoverWidth = "bookshelfGridCoverWidth"
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape" const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
@@ -13,7 +13,6 @@ import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -34,6 +33,7 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchScope
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.widget.dialog.WaitDialog import io.legado.app.ui.widget.dialog.WaitDialog
@@ -83,14 +83,15 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
private var currentSelectedSearchBook: SearchBook? = null private var currentSelectedSearchBook: SearchBook? = null
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = { private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
if (it) { if (it) {
val searchGroup = AppConfig.searchGroup val searchScope = SearchScope(ChangeSourceConfig.searchScope)
if (searchGroup.isNotEmpty()) { val group = searchScope.display
if (!searchScope.isAll()) {
lifecycleScope.launch { lifecycleScope.launch {
context?.alert("搜索结果为空") { context?.alert("搜索结果为空") {
setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组") setMessage("${group}分组搜索结果为空,是否切换到全部分组")
cancelButton() cancelButton()
okButton { okButton {
AppConfig.searchGroup = "" ChangeSourceConfig.searchScope = ""
upGroupMenuName() upGroupMenuName()
viewModel.startSearch() viewModel.startSearch()
} }
@@ -343,9 +344,9 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
else -> if (item?.groupId == R.id.source_group && !item.isChecked) { else -> if (item?.groupId == R.id.source_group && !item.isChecked) {
item.isChecked = true item.isChecked = true
if (item.title.toString() == getString(R.string.all_source)) { if (item.title.toString() == getString(R.string.all_source)) {
AppConfig.searchGroup = "" ChangeSourceConfig.searchScope = ""
} else { } else {
AppConfig.searchGroup = item.title.toString() ChangeSourceConfig.searchScope = item.title.toString()
} }
upGroupMenuName() upGroupMenuName()
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
@@ -493,7 +494,8 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
private fun upGroupMenu() { private fun upGroupMenu() {
binding.toolBar.menu.findItem(R.id.menu_group)?.run { binding.toolBar.menu.findItem(R.id.menu_group)?.run {
subMenu?.transaction { menu -> subMenu?.transaction { menu ->
val selectedGroup = AppConfig.searchGroup val searchScope = SearchScope(ChangeSourceConfig.searchScope)
val selectedGroup = searchScope.displayNames.firstOrNull() ?: ""
menu.removeGroup(R.id.source_group) menu.removeGroup(R.id.source_group)
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
var hasSelectedGroup = false var hasSelectedGroup = false
@@ -507,7 +509,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
} }
menu.setGroupCheckable(R.id.source_group, true, true) menu.setGroupCheckable(R.id.source_group, true, true)
if (hasSelectedGroup) { if (hasSelectedGroup) {
title = getString(R.string.group) + "(" + AppConfig.searchGroup + ")" title = getString(R.string.group) + "(" + selectedGroup + ")"
} else { } else {
allItem.isChecked = true allItem.isChecked = true
title = getString(R.string.group) title = getString(R.string.group)
@@ -521,11 +523,11 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
*/ */
private fun upGroupMenuName() { private fun upGroupMenuName() {
val menuGroup = binding.toolBar.menu.findItem(R.id.menu_group) val menuGroup = binding.toolBar.menu.findItem(R.id.menu_group)
val searchGroup = AppConfig.searchGroup val searchScope = SearchScope(ChangeSourceConfig.searchScope)
if (searchGroup.isEmpty()) { if (searchScope.isAll()) {
menuGroup?.title = getString(R.string.group) menuGroup?.title = getString(R.string.group)
} else { } else {
menuGroup?.title = getString(R.string.group) + "($searchGroup)" menuGroup?.title = getString(R.string.group) + "(${searchScope.display})"
} }
} }
@@ -38,7 +38,6 @@ import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -411,25 +410,24 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
} }
private fun getDbSearchBooks(): List<SearchBook> { private fun getDbSearchBooks(): List<SearchBook> {
val searchScope = io.legado.app.ui.book.search.SearchScope(
ChangeSourceConfig.searchScope
)
val group = when {
searchScope.isAll() || searchScope.isSource() -> ""
else -> searchScope.displayNames.firstOrNull() ?: ""
}
return if (screenKey.isEmpty()) { return if (screenKey.isEmpty()) {
if (AppConfig.changeSourceCheckAuthor) { if (AppConfig.changeSourceCheckAuthor) {
appDb.searchBookDao.changeSourceByGroup( appDb.searchBookDao.changeSourceByGroup(name, author, group)
name, author, AppConfig.searchGroup
)
} else { } else {
appDb.searchBookDao.changeSourceByGroup( appDb.searchBookDao.changeSourceByGroup(name, "", group)
name, "", AppConfig.searchGroup
)
} }
} else { } else {
if (AppConfig.changeSourceCheckAuthor) { if (AppConfig.changeSourceCheckAuthor) {
appDb.searchBookDao.changeSourceSearch( appDb.searchBookDao.changeSourceSearch(name, author, screenKey, group)
name, author, screenKey, AppConfig.searchGroup
)
} else { } else {
appDb.searchBookDao.changeSourceSearch( appDb.searchBookDao.changeSourceSearch(name, "", screenKey, group)
name, "", screenKey, AppConfig.searchGroup
)
} }
} }
} }
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.changesource package io.legado.app.ui.book.changesource
//import io.legado.app.lib.theme.primaryColor
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -13,7 +14,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.legado.app.R import io.legado.app.R
@@ -29,8 +29,8 @@ import io.legado.app.databinding.DialogChapterChangeSourceBinding
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.search.SearchScope
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.ui.widget.recycler.VerticalDivider
@@ -88,14 +88,15 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
private var searchBook: SearchBook? = null private var searchBook: SearchBook? = null
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = { private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
if (it) { if (it) {
val searchGroup = AppConfig.searchGroup val searchScope = SearchScope(ChangeSourceConfig.searchScope)
if (searchGroup.isNotEmpty()) { val group = searchScope.display
if (!searchScope.isAll()) {
lifecycleScope.launch { lifecycleScope.launch {
context?.alert("搜索结果为空") { context?.alert("搜索结果为空") {
setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组") setMessage("${group}分组搜索结果为空,是否切换到全部分组")
noButton() noButton()
yesButton { yesButton {
AppConfig.searchGroup = "" ChangeSourceConfig.searchScope = ""
upGroupMenu() upGroupMenu()
viewModel.startSearch() viewModel.startSearch()
} }
@@ -282,9 +283,9 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
else -> if (item?.groupId == R.id.source_group && !item.isChecked) { else -> if (item?.groupId == R.id.source_group && !item.isChecked) {
item.isChecked = true item.isChecked = true
if (item.title.toString() == getString(R.string.all_source)) { if (item.title.toString() == getString(R.string.all_source)) {
AppConfig.searchGroup = "" ChangeSourceConfig.searchScope = ""
} else { } else {
AppConfig.searchGroup = item.title.toString() ChangeSourceConfig.searchScope = item.title.toString()
} }
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
viewModel.stopSearch() viewModel.stopSearch()
@@ -380,7 +381,8 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
*/ */
private fun upGroupMenu() { private fun upGroupMenu() {
binding.toolBar.menu.findItem(R.id.menu_group)?.subMenu?.transaction { menu -> binding.toolBar.menu.findItem(R.id.menu_group)?.subMenu?.transaction { menu ->
val selectedGroup = AppConfig.searchGroup val searchScope = SearchScope(ChangeSourceConfig.searchScope)
val selectedGroup = searchScope.displayNames.firstOrNull() ?: ""
menu.removeGroup(R.id.source_group) menu.removeGroup(R.id.source_group)
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source) val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
var hasSelectedGroup = false var hasSelectedGroup = false
@@ -6,11 +6,6 @@ import io.legado.app.ui.config.prefDelegate
object ChangeSourceConfig { object ChangeSourceConfig {
var searchGroup by prefDelegate(
key = "searchGroup",
defaultValue = ""
)
var searchScope by prefDelegate( var searchScope by prefDelegate(
key = "changeSourceSearchScope", key = "changeSourceSearchScope",
defaultValue = "" defaultValue = ""
@@ -35,8 +35,6 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Timeline import androidx.compose.material.icons.filled.Timeline
import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.Book
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.ExtendedFloatingActionButton
@@ -938,16 +936,12 @@ private fun BookInfoSummary(
} }
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
book.remark?.takeIf { it.isNotBlank() }?.let { remark -> book.remark?.takeIf { it.isNotBlank() }?.let { remark ->
ElevatedCard( GlassCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onRemarkClick, onClick = onRemarkClick,
colors = CardDefaults.elevatedCardColors( containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
contentColor = LegadoTheme.colorScheme.onSurface,
)
) { ) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
AppText(text = stringResource(R.string.book_remark), style = LegadoTheme.typography.titleSmall)
AppText(text = remark, style = LegadoTheme.typography.labelMediumEmphasized) AppText(text = remark, style = LegadoTheme.typography.labelMediumEmphasized)
} }
} }
@@ -760,7 +760,7 @@ fun ChapterItem(
text = item.title, text = item.title,
style = LegadoTheme.typography.bodyMediumEmphasized.copy(fontWeight = FontWeight.Medium), style = LegadoTheme.typography.bodyMediumEmphasized.copy(fontWeight = FontWeight.Medium),
color = textColor, color = textColor,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
} }
@@ -779,7 +779,7 @@ fun ChapterItem(
val showStatusIcon = val showStatusIcon =
remember(item.isDur, item.downloadState, item.wordCount, showWordCount) { remember(item.isDur, item.downloadState, item.wordCount, showWordCount) {
if (item.downloadState == DownloadState.LOCAL) { if (item.downloadState == DownloadState.LOCAL) {
showWordCount && !item.wordCount.isNullOrEmpty() item.isDur || (showWordCount && !item.wordCount.isNullOrEmpty())
} else { } else {
true true
} }
@@ -888,15 +888,12 @@ private fun StatusIcon(
) { ) {
val targetState = when { val targetState = when {
downloadState == DownloadState.LOCAL -> { showWordCount && !wordCount.isNullOrEmpty() && (downloadState == DownloadState.LOCAL || downloadState == DownloadState.SUCCESS) -> "SUCCESS_WORD_COUNT"
if (showWordCount && !wordCount.isNullOrEmpty()) "SUCCESS_WORD_COUNT" else "EMPTY"
}
isDur -> "DUR" isDur -> "DUR"
downloadState == DownloadState.DOWNLOADING -> "LOADING" downloadState == DownloadState.DOWNLOADING -> "LOADING"
downloadState == DownloadState.SUCCESS && showWordCount && !wordCount.isNullOrEmpty() -> "SUCCESS_WORD_COUNT"
downloadState == DownloadState.SUCCESS -> "SUCCESS_ICON" downloadState == DownloadState.SUCCESS -> "SUCCESS_ICON"
downloadState == DownloadState.ERROR -> "ERROR" downloadState == DownloadState.ERROR -> "ERROR"
downloadState == DownloadState.LOCAL -> "EMPTY"
else -> "NONE" else -> "NONE"
} }
@@ -933,7 +930,7 @@ private fun StatusIcon(
"SUCCESS_WORD_COUNT" -> { "SUCCESS_WORD_COUNT" -> {
NormalCard( NormalCard(
cornerRadius = 12.dp, cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainer containerColor = if (isDur) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer
) { ) {
if (wordCount != null) { if (wordCount != null) {
AppText( AppText(
@@ -941,7 +938,7 @@ private fun StatusIcon(
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
text = wordCount, text = wordCount,
style = LegadoTheme.typography.labelSmall.copy(fontSize = 9.sp), style = LegadoTheme.typography.labelSmall.copy(fontSize = 9.sp),
color = LegadoTheme.colorScheme.onSurfaceVariant color = if (isDur) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
@@ -23,6 +24,9 @@ import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.GridView
@@ -33,6 +37,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -227,14 +232,24 @@ fun HomepageScreen(
data = errorMsg, data = errorMsg,
onDismissRequest = { errorMsg = null }, onDismissRequest = { errorMsg = null },
title = stringResource(R.string.homepage_module_error), title = stringResource(R.string.homepage_module_error),
text = errorMsg,
confirmText = stringResource(R.string.copy_text), confirmText = stringResource(R.string.copy_text),
onConfirm = { onConfirm = {
context.sendToClip(it) context.sendToClip(it)
errorMsg = null errorMsg = null
}, },
dismissText = stringResource(R.string.close), dismissText = stringResource(R.string.close),
onDismiss = { errorMsg = null } onDismiss = { errorMsg = null },
content = { msg ->
SelectionContainer {
Text(
text = msg,
style = LegadoTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
)
}
}
) )
HomepageModuleManageSheet( HomepageModuleManageSheet(
@@ -7,11 +7,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -52,14 +57,24 @@ fun LoadMoreFooter(
data = showFullError, data = showFullError,
onDismissRequest = { showFullError = null }, onDismissRequest = { showFullError = null },
title = "错误详情", title = "错误详情",
textProvider = { this },
confirmText = "复制", confirmText = "复制",
onConfirm = { error -> onConfirm = { error ->
context.sendToClip(error) context.sendToClip(error)
showFullError = null showFullError = null
}, },
dismissText = "关闭", dismissText = "关闭",
onDismiss = { showFullError = null } onDismiss = { showFullError = null },
content = { error ->
SelectionContainer {
Text(
text = error,
style = LegadoTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
)
}
}
) )
Box( Box(
@@ -12,8 +12,12 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
@@ -55,12 +59,14 @@ fun SearchBar(
val textFieldState = rememberTextFieldState(initialText = query) val textFieldState = rememberTextFieldState(initialText = query)
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
var hasFocused by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(autoFocus) { LaunchedEffect(autoFocus) {
if (autoFocus) { if (autoFocus && !hasFocused) {
focusRequester.requestFocus() focusRequester.requestFocus()
// 某些情况下需要手动调用 show() 确保键盘弹出 // 某些情况下需要手动调用 show() 确保键盘弹出
keyboardController?.show() keyboardController?.show()
hasFocused = true
} }
} }
@@ -54,6 +54,7 @@ fun AppAlertDialog(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 12.dp) .padding(top = 12.dp)
.verticalScroll(rememberScrollState())
) { ) {
content() content()
} }
@@ -13,6 +13,7 @@ import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.domain.usecase.ExploreKindUiUseCase import io.legado.app.domain.usecase.ExploreKindUiUseCase
import io.legado.app.help.source.getExploreInfoMap import io.legado.app.help.source.getExploreInfoMap
import io.legado.app.utils.InfoMap import io.legado.app.utils.InfoMap
import io.legado.app.utils.showDialogFragment
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -68,6 +69,10 @@ class ExploreKindItemState(
?: kind.title ?: kind.title
} }
} }
fun showError(error: String) {
activity?.showDialogFragment(io.legado.app.ui.widget.dialog.TextDialog("ERROR", error))
}
} }
@Composable @Composable
@@ -1,9 +1,14 @@
package io.legado.app.ui.widget.components.explore package io.legado.app.ui.widget.components.explore
import android.content.Context
import android.content.ContextWrapper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
@@ -18,17 +23,19 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.domain.usecase.ExploreKindUiUseCase import io.legado.app.domain.usecase.ExploreKindUiUseCase
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.showDialogFragment import io.legado.app.utils.sendToClip
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -52,84 +59,55 @@ fun ExploreKindMultiTypeItem(
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? = null content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? = null
) { ) {
val state = rememberExploreKindItemState(kind, sourceUrl, useCase, activity, onRefreshKinds) val context = LocalContext.current
val currentActivity = remember(context, activity) { activity ?: context.findActivity() }
val state =
rememberExploreKindItemState(kind, sourceUrl, useCase, currentActivity, onRefreshKinds)
state.ResolveDisplayName(displayNameOverride) state.ResolveDisplayName(displayNameOverride)
var showFullError by remember { mutableStateOf<String?>(null) }
val errorMsg = remember(kind) { kind.errorMsg }
val trailingIcon = rememberTrailingIcon(kind.type, isSelected) val trailingIcon = rememberTrailingIcon(kind.type, isSelected)
if (onClick != null) { val BaseItem = @Composable { text: String, click: () -> Unit ->
if (content != null) { if (content != null) {
content(state.displayName, isSelected, onClick, trailingIcon) content(text, isSelected, click, trailingIcon)
} else { } else {
ExploreKindItem( ExploreKindItem(
kind = kind, kind = kind,
isClickable = true, isClickable = true,
onClick = onClick, onClick = click,
modifier = modifier, modifier = modifier,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
isMiuix = isMiuix, isMiuix = isMiuix,
displayText = state.displayName, displayText = text,
isSelected = isSelected, isSelected = isSelected,
trailingIcon = trailingIcon trailingIcon = trailingIcon
) )
} }
return
} }
when (kind.type) { when {
ExploreKind.Type.url -> { errorMsg != null -> BaseItem(state.displayName) { showFullError = errorMsg }
val url = kind.url?.takeIf { it.isNotBlank() }
val internalOnClick = { onClick != null -> BaseItem(state.displayName, onClick)
if (!url.isNullOrBlank()) {
if (kind.title.startsWith("ERROR:")) { else -> when (kind.type) {
activity?.showDialogFragment(TextDialog("ERROR", url)) ExploreKind.Type.url -> {
} else { val url = kind.url?.takeIf { it.isNotBlank() }
onOpenUrl(url) BaseItem(state.displayName) {
} if (!url.isNullOrBlank()) onOpenUrl(url)
} }
} }
if (content != null) {
content(state.displayName, isSelected, internalOnClick, trailingIcon)
} else {
ExploreKindItem(
kind = kind,
isClickable = !url.isNullOrBlank(),
onClick = internalOnClick,
modifier = modifier,
backgroundColor = backgroundColor,
isMiuix = isMiuix,
displayText = state.displayName,
isSelected = isSelected
)
}
}
ExploreKind.Type.button -> { ExploreKind.Type.button -> {
val internalOnClick = { BaseItem(state.displayName) {
if (onRunAction != null) onRunAction() if (onRunAction != null) onRunAction()
else state.executeAction(kind.action) else state.executeAction(kind.action)
}
} }
if (content != null) {
content(state.displayName, isSelected, internalOnClick, trailingIcon)
} else {
ExploreKindItem(
kind = kind,
isClickable = !kind.action.isNullOrBlank(),
onClick = internalOnClick,
modifier = modifier,
backgroundColor = backgroundColor,
isMiuix = isMiuix,
displayText = state.displayName,
isSelected = isSelected,
trailingIcon = trailingIcon
)
}
}
ExploreKind.Type.text -> { ExploreKind.Type.text -> {
if (content != null) {
content(state.displayName, isSelected, {}, null)
} else {
TextTypeItem( TextTypeItem(
kind, kind,
sourceUrl, sourceUrl,
@@ -140,56 +118,73 @@ fun ExploreKindMultiTypeItem(
backgroundColor backgroundColor
) )
} }
}
ExploreKind.Type.toggle -> { ExploreKind.Type.toggle -> {
ToggleTypeItem( ToggleTypeItem(kind, sourceUrl, state, valueOverride, onValueChange, BaseItem)
kind, }
sourceUrl,
state,
valueOverride,
onValueChange,
isSelected,
modifier,
backgroundColor,
isMiuix,
trailingIcon,
content
)
}
ExploreKind.Type.select -> { ExploreKind.Type.select -> {
SelectTypeItem( SelectTypeItem(kind, sourceUrl, state, valueOverride, onValueChange, BaseItem)
kind, }
sourceUrl,
state,
valueOverride,
onValueChange,
isSelected,
modifier,
backgroundColor,
isMiuix,
trailingIcon,
content
)
}
else -> { else -> {
if (content != null) { if (content != null) {
content(state.displayName, isSelected, {}, null) content(state.displayName, isSelected, {}, null)
} else { } else {
ExploreKindItem( ExploreKindItem(
kind = kind, kind = kind,
isClickable = false, isClickable = false,
onClick = {}, onClick = {},
modifier = modifier, modifier = modifier,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
isMiuix = isMiuix, isMiuix = isMiuix,
displayText = state.displayName displayText = state.displayName
) )
}
} }
} }
} }
AppAlertDialog(
data = showFullError,
onDismissRequest = { showFullError = null },
title = "错误详情",
confirmText = "复制",
onConfirm = { error ->
context.sendToClip(error)
showFullError = null
},
dismissText = "关闭",
onDismiss = { showFullError = null },
content = { error ->
SelectionContainer {
AppText(
text = error,
style = LegadoTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 400.dp)
.verticalScroll(rememberScrollState())
)
}
}
)
}
private val ExploreKind.errorMsg: String?
get() = when {
title.startsWith("ERROR:", ignoreCase = true) -> url ?: title
url?.contains("Exception") == true -> url
title.contains("Exception") -> title
else -> null
}
private fun Context.findActivity(): AppCompatActivity? {
var context = this
while (context is ContextWrapper) {
if (context is AppCompatActivity) return context
context = context.baseContext
}
return null
} }
@Composable @Composable
@@ -236,12 +231,7 @@ private fun ToggleTypeItem(
state: ExploreKindItemState, state: ExploreKindItemState,
valueOverride: String?, valueOverride: String?,
onValueChange: ((String) -> Unit)?, onValueChange: ((String) -> Unit)?,
isSelected: Boolean, BaseItem: @Composable (String, () -> Unit) -> Unit
modifier: Modifier,
backgroundColor: Color,
isMiuix: Boolean,
trailingIcon: @Composable (() -> Unit)?,
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
) { ) {
val chars = remember(kind.chars) { val chars = remember(kind.chars) {
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
@@ -269,21 +259,7 @@ private fun ToggleTypeItem(
state.executeAction(kind.action) state.executeAction(kind.action)
} }
if (content != null) { BaseItem(text, internalOnClick)
content(text, isSelected, internalOnClick, trailingIcon)
} else {
ExploreKindItem(
kind = kind,
isClickable = true,
onClick = internalOnClick,
modifier = modifier,
backgroundColor = backgroundColor,
isMiuix = isMiuix,
displayText = text,
isSelected = isSelected,
trailingIcon = trailingIcon
)
}
} }
@Composable @Composable
@@ -293,12 +269,7 @@ private fun SelectTypeItem(
state: ExploreKindItemState, state: ExploreKindItemState,
valueOverride: String?, valueOverride: String?,
onValueChange: ((String) -> Unit)?, onValueChange: ((String) -> Unit)?,
isSelected: Boolean, BaseItem: @Composable (String, () -> Unit) -> Unit
modifier: Modifier,
backgroundColor: Color,
isMiuix: Boolean,
trailingIcon: @Composable (() -> Unit)?,
content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
) { ) {
val chars = remember(kind.chars) { val chars = remember(kind.chars) {
kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
@@ -318,25 +289,11 @@ private fun SelectTypeItem(
} }
var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) } var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
Box(modifier = modifier) { Box {
val internalOnClick = { showSelector = true } val internalOnClick = { showSelector = true }
val displayText = "${state.displayName} $selected" val displayText = "${state.displayName} $selected"
if (content != null) { BaseItem(displayText, internalOnClick)
content(displayText, isSelected, internalOnClick, trailingIcon)
} else {
ExploreKindItem(
kind = kind,
isClickable = chars.isNotEmpty(),
onClick = internalOnClick,
modifier = Modifier.fillMaxWidth(),
backgroundColor = backgroundColor,
isMiuix = isMiuix,
displayText = displayText,
isSelected = isSelected,
trailingIcon = trailingIcon
)
}
RoundDropdownMenu( RoundDropdownMenu(
expanded = showSelector, expanded = showSelector,