HapeLee 5 сар өмнө
parent
commit
7b32cb732f

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -274,6 +274,7 @@ object PreferKey {
     const val bookshelfGroupListStyle = "bookshelfGroupListStyle"
     const val bookshelfGroupCoverCount = "bookshelfGroupCoverCount"
     const val bookshelfListCoverWidth = "bookshelfListCoverWidth"
+    const val bookshelfGridCoverWidth = "bookshelfGridCoverWidth"
 
     const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
 

+ 14 - 12
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt

@@ -13,7 +13,6 @@ import androidx.appcompat.widget.SearchView
 import androidx.appcompat.widget.Toolbar
 import androidx.core.os.bundleOf
 import androidx.core.view.isInvisible
-import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle.State.STARTED
 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.model.ReadBook
 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.manage.BookSourceActivity
 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 val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchGroup = AppConfig.searchGroup
-            if (searchGroup.isNotEmpty()) {
+            val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+            val group = searchScope.display
+            if (!searchScope.isAll()) {
                 lifecycleScope.launch {
                     context?.alert("搜索结果为空") {
-                        setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
+                        setMessage("${group}分组搜索结果为空,是否切换到全部分组")
                         cancelButton()
                         okButton {
-                            AppConfig.searchGroup = ""
+                            ChangeSourceConfig.searchScope = ""
                             upGroupMenuName()
                             viewModel.startSearch()
                         }
@@ -343,9 +344,9 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
             else -> if (item?.groupId == R.id.source_group && !item.isChecked) {
                 item.isChecked = true
                 if (item.title.toString() == getString(R.string.all_source)) {
-                    AppConfig.searchGroup = ""
+                    ChangeSourceConfig.searchScope = ""
                 } else {
-                    AppConfig.searchGroup = item.title.toString()
+                    ChangeSourceConfig.searchScope = item.title.toString()
                 }
                 upGroupMenuName()
                 lifecycleScope.launch(IO) {
@@ -493,7 +494,8 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
     private fun upGroupMenu() {
         binding.toolBar.menu.findItem(R.id.menu_group)?.run {
             subMenu?.transaction { menu ->
-                val selectedGroup = AppConfig.searchGroup
+                val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+                val selectedGroup = searchScope.displayNames.firstOrNull() ?: ""
                 menu.removeGroup(R.id.source_group)
                 val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
                 var hasSelectedGroup = false
@@ -507,7 +509,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
                 }
                 menu.setGroupCheckable(R.id.source_group, true, true)
                 if (hasSelectedGroup) {
-                    title = getString(R.string.group) + "(" + AppConfig.searchGroup + ")"
+                    title = getString(R.string.group) + "(" + selectedGroup + ")"
                 } else {
                     allItem.isChecked = true
                     title = getString(R.string.group)
@@ -521,11 +523,11 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
      */
     private fun upGroupMenuName() {
         val menuGroup = binding.toolBar.menu.findItem(R.id.menu_group)
-        val searchGroup = AppConfig.searchGroup
-        if (searchGroup.isEmpty()) {
+        val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+        if (searchScope.isAll()) {
             menuGroup?.title = getString(R.string.group)
         } else {
-            menuGroup?.title = getString(R.string.group) + "($searchGroup)"
+            menuGroup?.title = getString(R.string.group) + "(${searchScope.display})"
         }
     }
 

+ 11 - 13
app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt

@@ -38,7 +38,6 @@ import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -411,25 +410,24 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
     }
 
     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()) {
             if (AppConfig.changeSourceCheckAuthor) {
-                appDb.searchBookDao.changeSourceByGroup(
-                    name, author, AppConfig.searchGroup
-                )
+                appDb.searchBookDao.changeSourceByGroup(name, author, group)
             } else {
-                appDb.searchBookDao.changeSourceByGroup(
-                    name, "", AppConfig.searchGroup
-                )
+                appDb.searchBookDao.changeSourceByGroup(name, "", group)
             }
         } else {
             if (AppConfig.changeSourceCheckAuthor) {
-                appDb.searchBookDao.changeSourceSearch(
-                    name, author, screenKey, AppConfig.searchGroup
-                )
+                appDb.searchBookDao.changeSourceSearch(name, author, screenKey, group)
             } else {
-                appDb.searchBookDao.changeSourceSearch(
-                    name, "", screenKey, AppConfig.searchGroup
-                )
+                appDb.searchBookDao.changeSourceSearch(name, "", screenKey, group)
             }
         }
     }

+ 11 - 9
app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.book.changesource
 
+//import io.legado.app.lib.theme.primaryColor
 import android.os.Bundle
 import android.view.Menu
 import android.view.MenuItem
@@ -13,7 +14,6 @@ import androidx.core.view.isVisible
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Lifecycle.State.STARTED
 import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 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.config.AppConfig
 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.search.SearchScope
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 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 val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
         if (it) {
-            val searchGroup = AppConfig.searchGroup
-            if (searchGroup.isNotEmpty()) {
+            val searchScope = SearchScope(ChangeSourceConfig.searchScope)
+            val group = searchScope.display
+            if (!searchScope.isAll()) {
                 lifecycleScope.launch {
                     context?.alert("搜索结果为空") {
-                        setMessage("${searchGroup}分组搜索结果为空,是否切换到全部分组")
+                        setMessage("${group}分组搜索结果为空,是否切换到全部分组")
                         noButton()
                         yesButton {
-                            AppConfig.searchGroup = ""
+                            ChangeSourceConfig.searchScope = ""
                             upGroupMenu()
                             viewModel.startSearch()
                         }
@@ -282,9 +283,9 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
             else -> if (item?.groupId == R.id.source_group && !item.isChecked) {
                 item.isChecked = true
                 if (item.title.toString() == getString(R.string.all_source)) {
-                    AppConfig.searchGroup = ""
+                    ChangeSourceConfig.searchScope = ""
                 } else {
-                    AppConfig.searchGroup = item.title.toString()
+                    ChangeSourceConfig.searchScope = item.title.toString()
                 }
                 lifecycleScope.launch(IO) {
                     viewModel.stopSearch()
@@ -380,7 +381,8 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
      */
     private fun upGroupMenu() {
         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)
             val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
             var hasSelectedGroup = false

+ 0 - 5
app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt

@@ -6,11 +6,6 @@ import io.legado.app.ui.config.prefDelegate
 
 object ChangeSourceConfig {
 
-    var searchGroup by prefDelegate(
-        key = "searchGroup",
-        defaultValue = ""
-    )
-
     var searchScope by prefDelegate(
         key = "changeSourceSearchScope",
         defaultValue = ""

+ 2 - 8
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -35,8 +35,6 @@ import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Share
 import androidx.compose.material.icons.filled.Timeline
 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.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExtendedFloatingActionButton
@@ -938,16 +936,12 @@ private fun BookInfoSummary(
         }
         Spacer(modifier = Modifier.height(4.dp))
         book.remark?.takeIf { it.isNotBlank() }?.let { remark ->
-            ElevatedCard(
+            GlassCard(
                 modifier = Modifier.fillMaxWidth(),
                 onClick = onRemarkClick,
-                colors = CardDefaults.elevatedCardColors(
-                    containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
-                    contentColor = LegadoTheme.colorScheme.onSurface,
-                )
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
             ) {
                 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)
                 }
             }

+ 6 - 9
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -760,7 +760,7 @@ fun ChapterItem(
                         text = item.title,
                         style = LegadoTheme.typography.bodyMediumEmphasized.copy(fontWeight = FontWeight.Medium),
                         color = textColor,
-                        maxLines = 1,
+                        maxLines = 2,
                         overflow = TextOverflow.Ellipsis
                     )
                 }
@@ -779,7 +779,7 @@ fun ChapterItem(
             val showStatusIcon =
                 remember(item.isDur, item.downloadState, item.wordCount, showWordCount) {
                     if (item.downloadState == DownloadState.LOCAL) {
-                        showWordCount && !item.wordCount.isNullOrEmpty()
+                        item.isDur || (showWordCount && !item.wordCount.isNullOrEmpty())
                     } else {
                         true
                     }
@@ -888,15 +888,12 @@ private fun StatusIcon(
 ) {
 
     val targetState = when {
-        downloadState == DownloadState.LOCAL -> {
-            if (showWordCount && !wordCount.isNullOrEmpty()) "SUCCESS_WORD_COUNT" else "EMPTY"
-        }
-
+        showWordCount && !wordCount.isNullOrEmpty() && (downloadState == DownloadState.LOCAL || downloadState == DownloadState.SUCCESS) -> "SUCCESS_WORD_COUNT"
         isDur -> "DUR"
         downloadState == DownloadState.DOWNLOADING -> "LOADING"
-        downloadState == DownloadState.SUCCESS && showWordCount && !wordCount.isNullOrEmpty() -> "SUCCESS_WORD_COUNT"
         downloadState == DownloadState.SUCCESS -> "SUCCESS_ICON"
         downloadState == DownloadState.ERROR -> "ERROR"
+        downloadState == DownloadState.LOCAL -> "EMPTY"
         else -> "NONE"
     }
 
@@ -933,7 +930,7 @@ private fun StatusIcon(
             "SUCCESS_WORD_COUNT" -> {
                 NormalCard(
                     cornerRadius = 12.dp,
-                    containerColor = LegadoTheme.colorScheme.surfaceContainer
+                    containerColor = if (isDur) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainer
                 ) {
                     if (wordCount != null) {
                         AppText(
@@ -941,7 +938,7 @@ private fun StatusIcon(
                                 .padding(horizontal = 8.dp, vertical = 4.dp),
                             text = wordCount,
                             style = LegadoTheme.typography.labelSmall.copy(fontSize = 9.sp),
-                            color = LegadoTheme.colorScheme.onSurfaceVariant
+                            color = if (isDur) LegadoTheme.colorScheme.onPrimaryContainer else LegadoTheme.colorScheme.onSurfaceVariant
                         )
                     }
                 }

+ 17 - 2
app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt

@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
 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.pager.HorizontalPager
 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.automirrored.filled.ArrowForward
 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.HorizontalDivider
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.derivedStateOf
@@ -227,14 +232,24 @@ fun HomepageScreen(
             data = errorMsg,
             onDismissRequest = { errorMsg = null },
             title = stringResource(R.string.homepage_module_error),
-            text = errorMsg,
             confirmText = stringResource(R.string.copy_text),
             onConfirm = {
                 context.sendToClip(it)
                 errorMsg = null
             },
             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(

+ 17 - 2
app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt

@@ -7,11 +7,16 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
 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.filled.Refresh
 import androidx.compose.material.icons.outlined.Info
 import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -52,14 +57,24 @@ fun LoadMoreFooter(
         data = showFullError,
         onDismissRequest = { showFullError = null },
         title = "错误详情",
-        textProvider = { this },
         confirmText = "复制",
         onConfirm = { error ->
             context.sendToClip(error)
             showFullError = null
         },
         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(

+ 7 - 1
app/src/main/java/io/legado/app/ui/widget/components/SearchBar.kt

@@ -12,8 +12,12 @@ import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
@@ -55,12 +59,14 @@ fun SearchBar(
     val textFieldState = rememberTextFieldState(initialText = query)
     val focusRequester = remember { FocusRequester() }
     val keyboardController = LocalSoftwareKeyboardController.current
+    var hasFocused by rememberSaveable { mutableStateOf(false) }
 
     LaunchedEffect(autoFocus) {
-        if (autoFocus) {
+        if (autoFocus && !hasFocused) {
             focusRequester.requestFocus()
             // 某些情况下需要手动调用 show() 确保键盘弹出
             keyboardController?.show()
+            hasFocused = true
         }
     }
 

+ 1 - 0
app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt

@@ -54,6 +54,7 @@ fun AppAlertDialog(
                         modifier = Modifier
                             .fillMaxWidth()
                             .padding(top = 12.dp)
+                            .verticalScroll(rememberScrollState())
                     ) {
                         content()
                     }

+ 5 - 0
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItemState.kt

@@ -13,6 +13,7 @@ import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.domain.usecase.ExploreKindUiUseCase
 import io.legado.app.help.source.getExploreInfoMap
 import io.legado.app.utils.InfoMap
+import io.legado.app.utils.showDialogFragment
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.launch
@@ -68,6 +69,10 @@ class ExploreKindItemState(
                         ?: kind.title
         }
     }
+
+    fun showError(error: String) {
+        activity?.showDialogFragment(io.legado.app.ui.widget.dialog.TextDialog("ERROR", error))
+    }
 }
 
 @Composable

+ 100 - 143
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt

@@ -1,9 +1,14 @@
 package io.legado.app.ui.widget.components.explore
 
+import android.content.Context
+import android.content.ContextWrapper
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
 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.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material.icons.filled.Refresh
@@ -18,17 +23,19 @@ import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.domain.usecase.ExploreKindUiUseCase
 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.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
-import io.legado.app.ui.widget.dialog.TextDialog
-import io.legado.app.utils.showDialogFragment
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.utils.sendToClip
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
@@ -52,84 +59,55 @@ fun ExploreKindMultiTypeItem(
     onClick: (() -> 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)
 
+    var showFullError by remember { mutableStateOf<String?>(null) }
+    val errorMsg = remember(kind) { kind.errorMsg }
     val trailingIcon = rememberTrailingIcon(kind.type, isSelected)
 
-    if (onClick != null) {
+    val BaseItem = @Composable { text: String, click: () -> Unit ->
         if (content != null) {
-            content(state.displayName, isSelected, onClick, trailingIcon)
+            content(text, isSelected, click, trailingIcon)
         } else {
             ExploreKindItem(
                 kind = kind,
                 isClickable = true,
-                onClick = onClick,
+                onClick = click,
                 modifier = modifier,
                 backgroundColor = backgroundColor,
                 isMiuix = isMiuix,
-                displayText = state.displayName,
+                displayText = text,
                 isSelected = isSelected,
                 trailingIcon = trailingIcon
             )
         }
-        return
     }
 
-    when (kind.type) {
-        ExploreKind.Type.url -> {
-            val url = kind.url?.takeIf { it.isNotBlank() }
-            val internalOnClick = {
-                if (!url.isNullOrBlank()) {
-                    if (kind.title.startsWith("ERROR:")) {
-                        activity?.showDialogFragment(TextDialog("ERROR", url))
-                    } else {
-                        onOpenUrl(url)
-                    }
+    when {
+        errorMsg != null -> BaseItem(state.displayName) { showFullError = errorMsg }
+
+        onClick != null -> BaseItem(state.displayName, onClick)
+
+        else -> when (kind.type) {
+            ExploreKind.Type.url -> {
+                val url = kind.url?.takeIf { it.isNotBlank() }
+                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 -> {
-            val internalOnClick = {
-                if (onRunAction != null) onRunAction()
-                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.button -> {
+                BaseItem(state.displayName) {
+                    if (onRunAction != null) onRunAction()
+                    else state.executeAction(kind.action)
+                }
             }
-        }
 
-        ExploreKind.Type.text -> {
-            if (content != null) {
-                content(state.displayName, isSelected, {}, null)
-            } else {
+            ExploreKind.Type.text -> {
                 TextTypeItem(
                     kind,
                     sourceUrl,
@@ -140,56 +118,73 @@ fun ExploreKindMultiTypeItem(
                     backgroundColor
                 )
             }
-        }
 
-        ExploreKind.Type.toggle -> {
-            ToggleTypeItem(
-                kind,
-                sourceUrl,
-                state,
-                valueOverride,
-                onValueChange,
-                isSelected,
-                modifier,
-                backgroundColor,
-                isMiuix,
-                trailingIcon,
-                content
-            )
-        }
+            ExploreKind.Type.toggle -> {
+                ToggleTypeItem(kind, sourceUrl, state, valueOverride, onValueChange, BaseItem)
+            }
 
-        ExploreKind.Type.select -> {
-            SelectTypeItem(
-                kind,
-                sourceUrl,
-                state,
-                valueOverride,
-                onValueChange,
-                isSelected,
-                modifier,
-                backgroundColor,
-                isMiuix,
-                trailingIcon,
-                content
-            )
+            ExploreKind.Type.select -> {
+                SelectTypeItem(kind, sourceUrl, state, valueOverride, onValueChange, BaseItem)
+            }
+
+            else -> {
+                if (content != null) {
+                    content(state.displayName, isSelected, {}, null)
+                } else {
+                    ExploreKindItem(
+                        kind = kind,
+                        isClickable = false,
+                        onClick = {},
+                        modifier = modifier,
+                        backgroundColor = backgroundColor,
+                        isMiuix = isMiuix,
+                        displayText = state.displayName
+                    )
+                }
+            }
         }
+    }
 
-        else -> {
-            if (content != null) {
-                content(state.displayName, isSelected, {}, null)
-            } else {
-                ExploreKindItem(
-                    kind = kind,
-                    isClickable = false,
-                    onClick = {},
-                    modifier = modifier,
-                    backgroundColor = backgroundColor,
-                    isMiuix = isMiuix,
-                    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
@@ -236,12 +231,7 @@ private fun ToggleTypeItem(
     state: ExploreKindItemState,
     valueOverride: String?,
     onValueChange: ((String) -> Unit)?,
-    isSelected: Boolean,
-    modifier: Modifier,
-    backgroundColor: Color,
-    isMiuix: Boolean,
-    trailingIcon: @Composable (() -> Unit)?,
-    content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
+    BaseItem: @Composable (String, () -> Unit) -> Unit
 ) {
     val chars = remember(kind.chars) {
         kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
@@ -269,21 +259,7 @@ private fun ToggleTypeItem(
         state.executeAction(kind.action)
     }
 
-    if (content != null) {
-        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
-        )
-    }
+    BaseItem(text, internalOnClick)
 }
 
 @Composable
@@ -293,12 +269,7 @@ private fun SelectTypeItem(
     state: ExploreKindItemState,
     valueOverride: String?,
     onValueChange: ((String) -> Unit)?,
-    isSelected: Boolean,
-    modifier: Modifier,
-    backgroundColor: Color,
-    isMiuix: Boolean,
-    trailingIcon: @Composable (() -> Unit)?,
-    content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)?
+    BaseItem: @Composable (String, () -> Unit) -> Unit
 ) {
     val chars = remember(kind.chars) {
         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) }
 
-    Box(modifier = modifier) {
+    Box {
         val internalOnClick = { showSelector = true }
         val displayText = "${state.displayName} $selected"
 
-        if (content != null) {
-            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
-            )
-        }
+        BaseItem(displayText, internalOnClick)
 
         RoundDropdownMenu(
             expanded = showSelector,