Răsfoiți Sursa

[优化] 支持发现页更多样式

HapeLee 7 luni în urmă
părinte
comite
49614f5ec6

+ 34 - 1
app/src/main/java/io/legado/app/data/entities/rule/ExploreKind.kt

@@ -6,11 +6,44 @@ package io.legado.app.data.entities.rule
 data class ExploreKind(
 data class ExploreKind(
     val title: String = "",
     val title: String = "",
     val url: String? = null,
     val url: String? = null,
+    val type: String = Type.url,
+    val action: String? = null,
+    val chars: Array<String?>? = null,
+    val default: String? = null,
+    var viewName: String? = null,
     val style: FlexChildStyle? = null
     val style: FlexChildStyle? = null
 ) {
 ) {
 
 
+    @Suppress("ConstPropertyName")
+    object Type {
+        const val url = "url"
+        const val text = "text"
+        const val button = "button"
+        const val toggle = "toggle"
+        const val select = "select"
+    }
+
     fun style(): FlexChildStyle {
     fun style(): FlexChildStyle {
         return style ?: FlexChildStyle.defaultStyle
         return style ?: FlexChildStyle.defaultStyle
     }
     }
 
 
-}
+    override fun equals(other: Any?): Boolean {
+        if (other is ExploreKind) {
+            return other.title == title
+                && other.type == type
+                && other.url == url
+                && other.action == action
+                && other.default == default
+        }
+        return false
+    }
+
+    override fun hashCode(): Int {
+        var result = title.hashCode() + type.hashCode()
+        result = 31 * result + (url?.hashCode() ?: 0)
+        result = 31 * result + (action?.hashCode() ?: 0)
+        result = 31 * result + (default?.hashCode() ?: 0)
+        return result
+    }
+
+}

+ 115 - 0
app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt

@@ -0,0 +1,115 @@
+package io.legado.app.domain.usecase
+
+import androidx.appcompat.app.AppCompatActivity
+import com.script.rhino.runScriptWithContext
+import io.legado.app.constant.AppLog
+import io.legado.app.data.dao.BookSourceDao
+import io.legado.app.data.entities.BaseSource
+import io.legado.app.data.entities.BookSource
+import io.legado.app.data.entities.rule.ExploreKind
+import io.legado.app.ui.login.SourceLoginJsExtensions
+import io.legado.app.utils.InfoMap
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.withContext
+import java.util.concurrent.ConcurrentHashMap
+
+class ExploreKindUiUseCase(
+    private val bookSourceDao: BookSourceDao
+) {
+
+    private val sourceCache: ConcurrentHashMap<String, BookSource?> = ConcurrentHashMap()
+
+    suspend fun warmUp(sourceUrl: String?) {
+        sourceUrl?.takeIf { it.isNotBlank() }?.let { getOrLoadBookSource(it) }
+    }
+
+    suspend fun resolveDisplayName(
+        kind: ExploreKind,
+        sourceUrl: String?,
+        infoMap: InfoMap?
+    ): String {
+        val viewName = kind.viewName
+        if (viewName.isNullOrBlank()) return kind.title
+        parseLiteralViewName(viewName)?.let { return it }
+
+        val effectiveSourceUrl = sourceUrl ?: return kind.title
+        val effectiveInfoMap = infoMap ?: return kind.title
+
+        return runCatching {
+            evalUiJs(viewName, effectiveSourceUrl, effectiveInfoMap)
+                .takeUnless { it.isNullOrEmpty() } ?: "null"
+        }.getOrElse {
+            "err"
+        }
+    }
+
+    suspend fun executeAction(
+        action: String?,
+        title: String,
+        sourceUrl: String?,
+        infoMap: InfoMap?,
+        activity: AppCompatActivity?,
+        onRefreshKinds: () -> Unit
+    ) {
+        val actionText = action?.takeIf { it.isNotBlank() } ?: return
+        val effectiveSourceUrl = sourceUrl ?: return
+        val effectiveInfoMap = infoMap ?: return
+        val source = getOrLoadBookSource(effectiveSourceUrl) ?: return
+        val sourceJsExtensions = SourceLoginJsExtensions(
+            activity = activity,
+            source = source,
+            callback = object : SourceLoginJsExtensions.Callback {
+                override fun upUiData(data: Map<String, String?>?) = Unit
+                override fun reUiView() = onRefreshKinds()
+            }
+        )
+        evalButtonClick(actionText, source, effectiveInfoMap, title, sourceJsExtensions)
+    }
+
+    private fun parseLiteralViewName(viewName: String?): String? {
+        if (viewName.isNullOrEmpty()) return null
+        return if (viewName.length in 3..19 && viewName.first() == '\'' && viewName.last() == '\'') {
+            viewName.substring(1, viewName.length - 1)
+        } else {
+            null
+        }
+    }
+
+    private suspend fun getOrLoadBookSource(sourceUrl: String): BookSource? {
+        sourceCache[sourceUrl]?.let { return it }
+        return withContext(IO) {
+            bookSourceDao.getBookSource(sourceUrl)
+        }?.also { source ->
+            sourceCache[sourceUrl] = source
+        }
+    }
+
+    private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? {
+        val source = getOrLoadBookSource(sourceUrl) ?: return null
+        return runScriptWithContext {
+            source.evalJS(jsStr) {
+                put("infoMap", infoMap)
+            }?.toString()
+        }
+    }
+
+    private suspend fun evalButtonClick(
+        jsStr: String,
+        source: BaseSource?,
+        infoMap: InfoMap,
+        name: String,
+        java: SourceLoginJsExtensions
+    ) {
+        val source = source ?: return
+        try {
+            runScriptWithContext {
+                source.evalJS(jsStr) {
+                    put("java", java)
+                    put("infoMap", infoMap)
+                }
+            }
+        } catch (e: Exception) {
+            AppLog.put("ExploreUI Button $name JavaScript error", e)
+        }
+    }
+}

+ 52 - 24
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt

@@ -2,6 +2,7 @@ package io.legado.app.ui.book.explore
 
 
 import android.annotation.SuppressLint
 import android.annotation.SuppressLint
 import android.content.Intent
 import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.Crossfade
 import androidx.compose.animation.Crossfade
@@ -31,7 +32,6 @@ import androidx.compose.foundation.lazy.grid.GridCells
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.GridItemSpan
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
 import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.foundation.lazy.grid.items
-import androidx.compose.foundation.lazy.grid.itemsIndexed
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
 import androidx.compose.foundation.lazy.grid.rememberLazyGridState
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
@@ -66,6 +66,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -73,6 +74,7 @@ import androidx.compose.ui.unit.sp
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.HazeState
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
+import io.legado.app.domain.usecase.ExploreKindUiUseCase
 import io.legado.app.model.BookShelfState
 import io.legado.app.model.BookShelfState
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
@@ -85,7 +87,8 @@ import io.legado.app.ui.widget.components.button.TopBarActionButton
 import io.legado.app.ui.widget.components.button.TopBarNavigationButton
 import io.legado.app.ui.widget.components.button.TopBarNavigationButton
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.cover.Cover
 import io.legado.app.ui.widget.components.cover.Cover
-import io.legado.app.ui.widget.components.explore.ExploreKindItem
+import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
+import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
 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.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -95,6 +98,7 @@ import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
 
 
 @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
 @SuppressLint("LocalContextConfigurationRead", "ConfigurationScreenWidthHeight")
 @OptIn(
 @OptIn(
@@ -131,6 +135,14 @@ fun ExploreShowScreen(
     var showGridCountSheet by remember { mutableStateOf(false) }
     var showGridCountSheet by remember { mutableStateOf(false) }
     val gridColumnCount by viewModel.gridCount.collectAsState()
     val gridColumnCount by viewModel.gridCount.collectAsState()
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    val context = LocalContext.current
+    val activity = context as? AppCompatActivity
+    val sourceUrl = remember(intent) { intent.getStringExtra("sourceUrl") }
+    val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
+
+    LaunchedEffect(sourceUrl) {
+        exploreKindUseCase.warmUp(sourceUrl)
+    }
 
 
     val isRefreshing by viewModel.isRefreshing.collectAsState()
     val isRefreshing by viewModel.isRefreshing.collectAsState()
     val pullToRefreshState = rememberPullToRefreshState()
     val pullToRefreshState = rememberPullToRefreshState()
@@ -242,7 +254,7 @@ fun ExploreShowScreen(
 
 
         SearchBarSection(
         SearchBarSection(
             query = kindQuery,
             query = kindQuery,
-            backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh,
+            backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
             onQueryChange = { kindQuery = it },
             onQueryChange = { kindQuery = it },
             placeholder = "选择或搜索分类",
             placeholder = "选择或搜索分类",
         )
         )
@@ -254,32 +266,48 @@ fun ExploreShowScreen(
                         (kind.url?.contains(kindQuery, ignoreCase = true) == true)
                         (kind.url?.contains(kindQuery, ignoreCase = true) == true)
             }
             }
         }
         }
+        val kindRows = remember(filteredKinds) {
+            calculateExploreKindRows(filteredKinds, 6)
+        }
 
 
-        LazyVerticalGrid(
-            columns = GridCells.Fixed(3),
+        LazyColumn(
             contentPadding = PaddingValues(vertical = 16.dp),
             contentPadding = PaddingValues(vertical = 16.dp),
-            horizontalArrangement = Arrangement.spacedBy(8.dp),
-            verticalArrangement = Arrangement.spacedBy(8.dp),
             modifier = Modifier.weight(1f, fill = false)
             modifier = Modifier.weight(1f, fill = false)
         ) {
         ) {
-            itemsIndexed(
-                items = filteredKinds,
-                key = { index, kind -> "${kind.url ?: kind.title}_$index" },
-                span = { _, kind ->
-                    val isClickable = !kind.url.isNullOrBlank()
-                    if (isClickable) GridItemSpan(1) else GridItemSpan(3)
+            items(kindRows) { rowItems ->
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .animateItem()
+                        .padding(vertical = 4.dp),
+                    horizontalArrangement = Arrangement.spacedBy(8.dp)
+                ) {
+                    rowItems.forEach { (kind, span) ->
+                        ExploreKindMultiTypeItem(
+                            modifier = Modifier
+                                .weight(span.toFloat())
+                                .animateItem(),
+                            kind = kind,
+                            sourceUrl = sourceUrl,
+                            activity = activity,
+                            onOpenUrl = { url ->
+                                showKindSheet = false
+                                viewModel.switchExploreUrl(kind.copy(url = url))
+                            },
+                            onRefreshKinds = viewModel::refreshKinds,
+                            backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
+                            isMiuix = isMiuix,
+                            useCase = exploreKindUseCase
+                        )
+                    }
+
+                    val totalSpan = rowItems.sumOf { it.second }
+                    if (totalSpan < 6) {
+                        Spacer(
+                            modifier = Modifier.weight((6 - totalSpan).toFloat())
+                        )
+                    }
                 }
                 }
-            ) { _, kind ->
-                ExploreKindItem(
-                    modifier = Modifier.animateItem(),
-                    kind = kind,
-                    isClickable = !kind.url.isNullOrBlank(),
-                    onClick = {
-                        showKindSheet = false
-                        viewModel.switchExploreUrl(kind)
-                    },
-                    isMiuix = isMiuix
-                )
             }
             }
         }
         }
     }
     }

+ 11 - 5
app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt

@@ -56,6 +56,7 @@ class ExploreShowViewModel(
     private val _isRefreshing = MutableStateFlow(false)
     private val _isRefreshing = MutableStateFlow(false)
     private val _errorMsg = MutableStateFlow<String?>(null)
     private val _errorMsg = MutableStateFlow<String?>(null)
     private var bookSource: BookSource? = null
     private var bookSource: BookSource? = null
+    private var sourceUrl: String? = null
     private var exploreUrl: String? = null
     private var exploreUrl: String? = null
     private var page = 1
     private var page = 1
     private val _isEndStateFlow = MutableStateFlow(false)
     private val _isEndStateFlow = MutableStateFlow(false)
@@ -110,13 +111,14 @@ class ExploreShowViewModel(
     }
     }
 
 
     fun initData(intent: Intent) {
     fun initData(intent: Intent) {
-        val sourceUrl = intent.getStringExtra("sourceUrl")
+        val incomingSourceUrl = intent.getStringExtra("sourceUrl")
+        sourceUrl = incomingSourceUrl
         exploreUrl = intent.getStringExtra("exploreUrl")
         exploreUrl = intent.getStringExtra("exploreUrl")
 
 
         viewModelScope.launch {
         viewModelScope.launch {
-            if (bookSource == null && sourceUrl != null) {
-                bookSource = repository.getBookSource(sourceUrl)
-                loadKinds(sourceUrl)
+            if (bookSource == null && incomingSourceUrl != null) {
+                bookSource = repository.getBookSource(incomingSourceUrl)
+                loadKinds(incomingSourceUrl)
             }
             }
             loadMore(isRefresh = true)
             loadMore(isRefresh = true)
         }
         }
@@ -128,6 +130,10 @@ class ExploreShowViewModel(
         }
         }
     }
     }
 
 
+    fun refreshKinds() {
+        sourceUrl?.let { loadKinds(it) }
+    }
+
     fun switchExploreUrl(kind: ExploreKind) {
     fun switchExploreUrl(kind: ExploreKind) {
         _selectedKindTitle.value = kind.title
         _selectedKindTitle.value = kind.title
         exploreUrl = kind.url
         exploreUrl = kind.url
@@ -221,4 +227,4 @@ class ExploreShowViewModel(
 
 
         return BookShelfState.NOT_IN_SHELF
         return BookShelfState.NOT_IN_SHELF
     }
     }
-}
+}

+ 29 - 54
app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.main.explore
 package io.legado.app.ui.main.explore
 
 
+import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.FastOutSlowInEasing
 import androidx.compose.animation.core.FastOutSlowInEasing
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.animateFloatAsState
@@ -50,7 +51,7 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.BookSourcePart
-import io.legado.app.data.entities.rule.ExploreKind
+import io.legado.app.domain.usecase.ExploreKindUiUseCase
 import io.legado.app.ui.book.explore.ExploreShowActivity
 import io.legado.app.ui.book.explore.ExploreShowActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.search.SearchScope
@@ -64,7 +65,8 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.divider.PillHeaderDivider
 import io.legado.app.ui.widget.components.divider.PillHeaderDivider
-import io.legado.app.ui.widget.components.explore.ExploreKindItem
+import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
+import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
@@ -75,8 +77,8 @@ import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.startActivity
 import io.legado.app.utils.startActivity
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
 import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.theme.MiuixTheme
-import kotlin.math.roundToInt
 
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
 @Composable
 @Composable
@@ -84,10 +86,12 @@ fun ExploreScreen(
     viewModel: ExploreViewModel = koinViewModel()
     viewModel: ExploreViewModel = koinViewModel()
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
+    val activity = context as? AppCompatActivity
     val uiState by viewModel.uiState.collectAsState()
     val uiState by viewModel.uiState.collectAsState()
     var sourceToDelete by remember { mutableStateOf<BookSourcePart?>(null) }
     var sourceToDelete by remember { mutableStateOf<BookSourcePart?>(null) }
     val listState = rememberLazyListState()
     val listState = rememberLazyListState()
     val scope = rememberCoroutineScope()
     val scope = rememberCoroutineScope()
+    val exploreKindUseCase: ExploreKindUiUseCase = koinInject()
 
 
     // 自动滚动置顶
     // 自动滚动置顶
     LaunchedEffect(uiState.expandedId) {
     LaunchedEffect(uiState.expandedId) {
@@ -113,7 +117,7 @@ fun ExploreScreen(
             var contentRowCount = 0
             var contentRowCount = 0
             for (item in uiState.items) {
             for (item in uiState.items) {
                 if (item.bookSourceUrl == expandedId) {
                 if (item.bookSourceUrl == expandedId) {
-                    contentRowCount = calculateRows(uiState.exploreKinds, 6).size
+                    contentRowCount = calculateExploreKindRows(uiState.exploreKinds, 6).size
                     break
                     break
                 }
                 }
                 headerIndex++
                 headerIndex++
@@ -167,6 +171,11 @@ fun ExploreScreen(
                     val isExpanded = uiState.expandedId == item.bookSourceUrl
                     val isExpanded = uiState.expandedId == item.bookSourceUrl
 
 
                     item(key = item.bookSourceUrl) {
                     item(key = item.bookSourceUrl) {
+                        if (isExpanded) {
+                            LaunchedEffect(item.bookSourceUrl) {
+                                exploreKindUseCase.warmUp(item.bookSourceUrl)
+                            }
+                        }
                         ExploreSourceHeader(
                         ExploreSourceHeader(
                             modifier = Modifier.animateItem(),
                             modifier = Modifier.animateItem(),
                             item = item,
                             item = item,
@@ -197,7 +206,7 @@ fun ExploreScreen(
                     }
                     }
 
 
                     if (isExpanded) {
                     if (isExpanded) {
-                        val rows = calculateRows(uiState.exploreKinds, 6)
+                        val rows = calculateExploreKindRows(uiState.exploreKinds, 6)
                         itemsIndexed(
                         itemsIndexed(
                             items = rows,
                             items = rows,
                             key = { index, _ -> "${item.bookSourceUrl}_$index" }
                             key = { index, _ -> "${item.bookSourceUrl}_$index" }
@@ -210,21 +219,21 @@ fun ExploreScreen(
                                 horizontalArrangement = Arrangement.spacedBy(8.dp)
                                 horizontalArrangement = Arrangement.spacedBy(8.dp)
                             ) {
                             ) {
                                 rowItems.forEach { (kind, span) ->
                                 rowItems.forEach { (kind, span) ->
-                                    val isClickable = !kind.url.isNullOrBlank()
-                                    ExploreKindItem(
+                                    ExploreKindMultiTypeItem(
                                         kind = kind,
                                         kind = kind,
-                                        isClickable = isClickable,
-                                        modifier = Modifier.weight(span.toFloat()),
-                                        onClick = {
-                                            if (isClickable) {
-                                                context.startActivity<ExploreShowActivity> {
-                                                    putExtra("exploreName", kind.title)
-                                                    putExtra("sourceUrl", item.bookSourceUrl)
-                                                    putExtra("exploreUrl", kind.url)
-                                                }
+                                        sourceUrl = item.bookSourceUrl,
+                                        activity = activity,
+                                        onOpenUrl = { url ->
+                                            context.startActivity<ExploreShowActivity> {
+                                                putExtra("exploreName", kind.title)
+                                                putExtra("sourceUrl", item.bookSourceUrl)
+                                                putExtra("exploreUrl", url)
                                             }
                                             }
                                         },
                                         },
-                                        isMiuix = composeEngine
+                                        onRefreshKinds = { viewModel.refreshExploreKinds(item) },
+                                        modifier = Modifier.weight(span.toFloat()),
+                                        isMiuix = composeEngine,
+                                        useCase = exploreKindUseCase
                                     )
                                     )
                                 }
                                 }
 
 
@@ -248,11 +257,11 @@ fun ExploreScreen(
             ) { item ->
             ) { item ->
                 TextCard(
                 TextCard(
                     text = item.bookSourceName,
                     text = item.bookSourceName,
-                    textStyle = LegadoTheme.typography.labelLarge,
+                    textStyle = LegadoTheme.typography.labelMediumEmphasized,
                     backgroundColor = LegadoTheme.colorScheme.cardContainer,
                     backgroundColor = LegadoTheme.colorScheme.cardContainer,
                     contentColor = LegadoTheme.colorScheme.onCardContainer,
                     contentColor = LegadoTheme.colorScheme.onCardContainer,
-                    cornerRadius = 8.dp,
-                    horizontalPadding = 8.dp,
+                    cornerRadius = 12.dp,
+                    horizontalPadding = 12.dp,
                     verticalPadding = 8.dp,
                     verticalPadding = 8.dp,
                     onClick = {
                     onClick = {
                         scope.launch {
                         scope.launch {
@@ -281,40 +290,6 @@ fun ExploreScreen(
     )
     )
 }
 }
 
 
-private fun calculateRows(
-    kinds: List<ExploreKind>,
-    maxSpan: Int
-): List<List<Pair<ExploreKind, Int>>> {
-    val rows = mutableListOf<MutableList<Pair<ExploreKind, Int>>>()
-    var currentRow = mutableListOf<Pair<ExploreKind, Int>>()
-    var currentSpan = 0
-    kinds.forEach { kind ->
-        val style = kind.style()
-        val span = when {
-            style.layout_wrapBefore || style.layout_flexBasisPercent >= 1.0f -> maxSpan
-            style.layout_flexBasisPercent > 0 -> (maxSpan * style.layout_flexBasisPercent).roundToInt()
-                .coerceIn(1, maxSpan)
-
-            style.layout_flexGrow > 0f -> 3
-            else -> 2
-        }
-        if ((style.layout_wrapBefore && currentRow.isNotEmpty()) || (currentSpan + span > maxSpan)) {
-            rows.add(currentRow)
-            currentRow = mutableListOf()
-            currentSpan = 0
-        }
-        currentRow.add(kind to span)
-        currentSpan += span
-        if (currentSpan >= maxSpan) {
-            rows.add(currentRow)
-            currentRow = mutableListOf()
-            currentSpan = 0
-        }
-    }
-    if (currentRow.isNotEmpty()) rows.add(currentRow)
-    return rows
-}
-
 @OptIn(ExperimentalFoundationApi::class)
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
 @Composable
 fun ExploreSourceHeader(
 fun ExploreSourceHeader(

+ 7 - 2
app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt

@@ -70,6 +70,11 @@ fun SearchBarSection(
     }
     }
 
 
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
+    val resolvedBackgroundColor = if (backgroundColor != Color.Unspecified) {
+        backgroundColor
+    } else {
+        if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow
+    }
 
 
     val modifier = modifier
     val modifier = modifier
         .fillMaxWidth()
         .fillMaxWidth()
@@ -88,7 +93,7 @@ fun SearchBarSection(
             backgroundColor = if (!isMiuix) {
             backgroundColor = if (!isMiuix) {
                 Color.Transparent
                 Color.Transparent
             } else {
             } else {
-                MiuixTheme.colorScheme.surfaceContainer
+                resolvedBackgroundColor
             }
             }
         )
         )
     }
     }
@@ -101,7 +106,7 @@ fun SearchBarSection(
                 .fillMaxWidth()
                 .fillMaxWidth()
                 .padding(bottom = 4.dp),
                 .padding(bottom = 4.dp),
             shape = RoundedCornerShape(32.dp),
             shape = RoundedCornerShape(32.dp),
-            color = backgroundColor
+            color = resolvedBackgroundColor
         ) {
         ) {
             searchTextField()
             searchTextField()
         }
         }

+ 45 - 38
app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.widget.components.explore
 package io.legado.app.ui.widget.components.explore
 
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.CardDefaults
@@ -8,6 +9,7 @@ import androidx.compose.material3.LocalMinimumInteractiveComponentSize
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
@@ -25,28 +27,11 @@ fun ExploreKindItem(
     isClickable: Boolean,
     isClickable: Boolean,
     onClick: () -> Unit,
     onClick: () -> Unit,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
-    isMiuix: Boolean
+    isMiuix: Boolean,
+    backgroundColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.surfaceContainer,
+    displayText: String = kind.title,
+    trailingIcon: (@Composable () -> Unit)? = null
 ) {
 ) {
-    val color = if (isMiuix)
-        MiuixTheme.colorScheme.surfaceContainer
-    else
-        MaterialTheme.colorScheme.secondaryContainer
-
-    val contentColor = if (isMiuix)
-        MiuixTheme.colorScheme.onSurface
-    else
-        MaterialTheme.colorScheme.secondary
-
-    val unClickBackColor = if (isMiuix)
-        MiuixTheme.colorScheme.surfaceContainer
-    else
-        MaterialTheme.colorScheme.surface
-
-    val unClickColor = if (isMiuix)
-        MiuixTheme.colorScheme.disabledOnSurface
-    else
-        MaterialTheme.colorScheme.primary
-
     CompositionLocalProvider(
     CompositionLocalProvider(
         LocalMinimumInteractiveComponentSize provides Dp.Unspecified
         LocalMinimumInteractiveComponentSize provides Dp.Unspecified
     ) {
     ) {
@@ -57,21 +42,29 @@ fun ExploreKindItem(
             GlassCard(
             GlassCard(
                 onClick = onClick,
                 onClick = onClick,
                 shape = shape,
                 shape = shape,
-                containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
+                containerColor = backgroundColor,
                 contentColor = LegadoTheme.colorScheme.onSurface,
                 contentColor = LegadoTheme.colorScheme.onSurface,
                 modifier = modifier
                 modifier = modifier
             ) {
             ) {
-                KindText(kind)
+                KindText(
+                    text = displayText,
+                    isClickable = true,
+                    trailingIcon = trailingIcon
+                )
             }
             }
         } else {
         } else {
             GlassCard(
             GlassCard(
                 shape = shape,
                 shape = shape,
-                containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.6f),
+                containerColor = backgroundColor,
                 contentColor = LegadoTheme.colorScheme.primary,
                 contentColor = LegadoTheme.colorScheme.primary,
                 modifier = modifier,
                 modifier = modifier,
                 border = CardDefaults.outlinedCardBorder()
                 border = CardDefaults.outlinedCardBorder()
             ) {
             ) {
-                KindText(kind)
+                KindText(
+                    text = displayText,
+                    isClickable = false,
+                    trailingIcon = trailingIcon
+                )
             }
             }
         }
         }
     }
     }
@@ -80,20 +73,34 @@ fun ExploreKindItem(
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 @Composable
 private fun KindText(
 private fun KindText(
-    kind: ExploreKind
+    text: String,
+    isClickable: Boolean,
+    trailingIcon: (@Composable () -> Unit)? = null
 ) {
 ) {
-    AppText(
-        text = kind.title,
-        color = if (kind.url.isNullOrBlank())
-            LegadoTheme.colorScheme.primary
-        else
-            LegadoTheme.colorScheme.onSurface,
+    Box(
         modifier = Modifier
         modifier = Modifier
             .fillMaxWidth()
             .fillMaxWidth()
-            .padding(vertical = 8.dp),
-        style = LegadoTheme.typography.labelMediumEmphasized,
-        textAlign = TextAlign.Center,
-        overflow = TextOverflow.Ellipsis,
-        maxLines = 1
-    )
+            .padding(vertical = 8.dp)
+    ) {
+        AppText(
+            text = text,
+            color = if (isClickable) LegadoTheme.colorScheme.onSurface else LegadoTheme.colorScheme.primary,
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(end = if (trailingIcon == null) 0.dp else 18.dp),
+            style = LegadoTheme.typography.labelMediumEmphasized,
+            textAlign = TextAlign.Center,
+            overflow = TextOverflow.Ellipsis,
+            maxLines = 1
+        )
+        if (trailingIcon != null) {
+            Box(
+                modifier = Modifier
+                    .align(Alignment.CenterEnd)
+                    .padding(end = 8.dp)
+            ) {
+                trailingIcon()
+            }
+        }
+    }
 }
 }

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

@@ -0,0 +1,68 @@
+package io.legado.app.ui.widget.components.explore
+
+import io.legado.app.data.entities.rule.ExploreKind
+import kotlin.math.roundToInt
+
+fun calculateExploreKindRows(
+    kinds: List<ExploreKind>,
+    maxSpan: Int
+): List<List<Pair<ExploreKind, Int>>> {
+    val rows = mutableListOf<MutableList<Pair<ExploreKind, Int>>>()
+    var currentRow = mutableListOf<Pair<ExploreKind, Int>>()
+    var currentSpan = 0
+
+    fun fillCurrentRowTail() {
+        if (currentRow.isEmpty()) return
+        val remain = maxSpan - currentSpan
+        if (remain <= 0) return
+        val allSameSpan = currentRow.map { it.second }.distinct().size == 1
+        if (allSameSpan && currentRow.size > 1) {
+            val addEach = remain / currentRow.size
+            var extra = remain % currentRow.size
+            currentRow.indices.forEach { index ->
+                val (kind, span) = currentRow[index]
+                val add = addEach + if (extra > 0) {
+                    extra -= 1
+                    1
+                } else {
+                    0
+                }
+                currentRow[index] = kind to (span + add)
+            }
+        } else {
+            val (lastKind, lastSpan) = currentRow.last()
+            currentRow[currentRow.lastIndex] = lastKind to (lastSpan + remain)
+        }
+        currentSpan += remain
+    }
+
+    kinds.forEach { kind ->
+        val style = kind.style()
+        val span = when {
+            style.layout_wrapBefore || style.layout_flexBasisPercent >= 1.0f -> maxSpan
+            style.layout_flexBasisPercent > 0 -> (maxSpan * style.layout_flexBasisPercent).roundToInt()
+                .coerceIn(1, maxSpan)
+
+            style.layout_flexGrow > 0f -> 3
+            else -> 2
+        }
+        if ((style.layout_wrapBefore && currentRow.isNotEmpty()) || (currentSpan + span > maxSpan)) {
+            fillCurrentRowTail()
+            rows.add(currentRow)
+            currentRow = mutableListOf()
+            currentSpan = 0
+        }
+        currentRow.add(kind to span)
+        currentSpan += span
+        if (currentSpan >= maxSpan) {
+            rows.add(currentRow)
+            currentRow = mutableListOf()
+            currentSpan = 0
+        }
+    }
+    if (currentRow.isNotEmpty()) {
+        fillCurrentRowTail()
+        rows.add(currentRow)
+    }
+    return rows
+}

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

@@ -0,0 +1,333 @@
+package io.legado.app.ui.widget.components.explore
+
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowRight
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
+import androidx.compose.material.icons.automirrored.filled.OpenInNew
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.UnfoldMore
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+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.setValue
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import io.legado.app.App
+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.ui.widget.components.menuItem.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.dialog.TextDialog
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.utils.showDialogFragment
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import top.yukonga.miuix.kmp.icon.MiuixIcons
+import top.yukonga.miuix.kmp.icon.basic.ArrowUpDown
+
+@Composable
+fun ExploreKindMultiTypeItem(
+    kind: ExploreKind,
+    sourceUrl: String?,
+    activity: AppCompatActivity?,
+    onOpenUrl: (String) -> Unit,
+    onRefreshKinds: () -> Unit,
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer,
+    isMiuix: Boolean,
+    useCase: ExploreKindUiUseCase
+) {
+    val scope = rememberCoroutineScope()
+    val infoMap = remember(sourceUrl) { sourceUrl?.takeIf { it.isNotBlank() }?.let(::getExploreInfoMap) }
+    var displayName by remember(sourceUrl, kind.title, kind.viewName) { mutableStateOf(kind.title) }
+
+    LaunchedEffect(sourceUrl, kind.title, kind.viewName) {
+        displayName = useCase.resolveDisplayName(kind, sourceUrl, infoMap)
+    }
+
+    fun runAction(action: String?) {
+        scope.launch(IO) {
+            useCase.executeAction(
+                action = action,
+                title = kind.title,
+                sourceUrl = sourceUrl,
+                infoMap = infoMap,
+                activity = activity,
+                onRefreshKinds = onRefreshKinds
+            )
+        }
+    }
+
+    when (kind.type) {
+        ExploreKind.Type.url -> {
+            val url = kind.url?.takeIf { it.isNotBlank() }
+            ExploreKindItem(
+                kind = kind,
+                isClickable = !url.isNullOrBlank(),
+                onClick = {
+                    if (url.isNullOrBlank()) return@ExploreKindItem
+                    if (kind.title.startsWith("ERROR:")) {
+                        activity?.showDialogFragment(TextDialog("ERROR", url))
+                    } else {
+                        onOpenUrl(url)
+                    }
+                },
+                modifier = modifier,
+                backgroundColor = backgroundColor,
+                isMiuix = isMiuix,
+                displayText = displayName
+            )
+        }
+
+        ExploreKind.Type.button -> {
+            ExploreKindItem(
+                kind = kind,
+                isClickable = !kind.action.isNullOrBlank(),
+                onClick = { runAction(kind.action) },
+                modifier = modifier,
+                backgroundColor = backgroundColor,
+                isMiuix = isMiuix,
+                displayText = displayName,
+                trailingIcon = {
+                    CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
+                        AppIcon(
+                            imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+                            contentDescription = null,
+                            modifier = Modifier.height(14.dp),
+                            tint = LegadoTheme.colorScheme.outlineVariant
+                        )
+                    }
+                }
+            )
+        }
+
+        ExploreKind.Type.text -> {
+            var value by remember(sourceUrl, kind.title) {
+                mutableStateOf(infoMap?.get(kind.title).orEmpty())
+            }
+            var actionJob by remember(sourceUrl, kind.title) { mutableStateOf<Job?>(null) }
+            ExploreKindCompactTextField(
+                value = value,
+                onValueChange = { newValue ->
+                    value = newValue
+                    infoMap?.let {
+                        it[kind.title] = newValue
+                        it.saveNow()
+                    }
+                    if (!kind.action.isNullOrBlank()) {
+                        actionJob?.cancel()
+                        actionJob = scope.launch {
+                            delay(600)
+                            runAction(kind.action)
+                        }
+                    }
+                },
+                placeholder = displayName,
+                modifier = modifier,
+                backgroundColor = backgroundColor,
+                isMiuix = isMiuix
+            )
+        }
+
+        ExploreKind.Type.toggle -> {
+            val chars = remember(kind.chars) {
+                kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
+            }
+            val left = kind.style().layout_justifySelf != "right"
+            var char by remember(sourceUrl, kind.title, kind.default, kind.chars) {
+                mutableStateOf(
+                    infoMap?.get(kind.title)
+                        ?.takeUnless { it.isEmpty() }
+                        ?: (kind.default ?: chars.first()).also {
+                            infoMap?.let { map ->
+                                map[kind.title] = it
+                                map.saveNow()
+                            }
+                        }
+                )
+            }
+            val text = if (left) "$char$displayName" else "$displayName$char"
+            ExploreKindItem(
+                kind = kind,
+                isClickable = true,
+                onClick = {
+                    val currentIndex = chars.indexOf(char)
+                    val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size
+                    char = chars.getOrElse(nextIndex) { "" }
+                    infoMap?.let { map ->
+                        map[kind.title] = char
+                        map.saveNow()
+                    }
+                    runAction(kind.action)
+                },
+                modifier = modifier,
+                backgroundColor = backgroundColor,
+                isMiuix = isMiuix,
+                displayText = text,
+                trailingIcon = {
+                    AppIcon(
+                        imageVector = Icons.Default.Refresh,
+                        contentDescription = null,
+                        modifier = Modifier.height(14.dp),
+                        tint = LegadoTheme.colorScheme.outlineVariant
+                    )
+                }
+            )
+        }
+
+        ExploreKind.Type.select -> {
+            val chars = remember(kind.chars) {
+                kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null")
+            }
+            var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) {
+                mutableStateOf(
+                    infoMap?.get(kind.title)
+                        ?.takeUnless { it.isEmpty() }
+                        ?: (kind.default ?: chars.first()).also {
+                            infoMap?.let { map ->
+                                map[kind.title] = it
+                                map.saveNow()
+                            }
+                        }
+                )
+            }
+            var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) }
+            Box(modifier = modifier) {
+                ExploreKindItem(
+                    kind = kind,
+                    isClickable = chars.isNotEmpty(),
+                    onClick = { showSelector = true },
+                    modifier = Modifier.fillMaxWidth(),
+                    backgroundColor = backgroundColor,
+                    isMiuix = isMiuix,
+                    displayText = "$displayName $selected",
+                    trailingIcon = {
+                        AppIcon(
+                            imageVector = Icons.Default.UnfoldMore,
+                            contentDescription = null,
+                            modifier = Modifier.height(14.dp),
+                            tint = LegadoTheme.colorScheme.outlineVariant
+                        )
+                    }
+                )
+                RoundDropdownMenu(
+                    expanded = showSelector,
+                    onDismissRequest = { showSelector = false }
+                ) {
+                    chars.forEach { option ->
+                        RoundDropdownMenuItem(
+                            text = option,
+                            onClick = {
+                                showSelector = false
+                                if (selected != option) {
+                                    selected = option
+                                    infoMap?.let { map ->
+                                        map[kind.title] = option
+                                        map.saveNow()
+                                    }
+                                    runAction(kind.action)
+                                }
+                            }
+                        )
+                    }
+                }
+            }
+        }
+
+        else -> {
+            ExploreKindItem(
+                kind = kind,
+                isClickable = false,
+                onClick = {},
+                modifier = modifier,
+                backgroundColor = backgroundColor,
+                isMiuix = isMiuix,
+                displayText = displayName
+            )
+        }
+    }
+}
+
+@Composable
+private fun ExploreKindCompactTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    placeholder: String,
+    modifier: Modifier = Modifier,
+    backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer,
+    isMiuix: Boolean
+) {
+    val interactionSource = remember { MutableInteractionSource() }
+    val isFocused by interactionSource.collectIsFocusedAsState()
+    val shape = RoundedCornerShape(10.dp)
+    val borderColor = if (isFocused) {
+        LegadoTheme.colorScheme.primary
+    } else {
+        Color.Transparent
+    }
+
+    BasicTextField(
+        value = value,
+        onValueChange = onValueChange,
+        singleLine = true,
+        textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface),
+        cursorBrush = SolidColor(LegadoTheme.colorScheme.primary),
+        interactionSource = interactionSource,
+        modifier = modifier
+            .height(34.dp)
+            .clip(shape)
+            .background(backgroundColor)
+            .border(width = 1.dp, color = borderColor, shape = shape),
+        decorationBox = { innerTextField ->
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(34.dp)
+                    .padding(horizontal = 10.dp),
+                contentAlignment = androidx.compose.ui.Alignment.CenterStart
+            ) {
+                if (value.isEmpty()) {
+                    AppText(
+                        text = placeholder,
+                        color = LegadoTheme.colorScheme.outline,
+                        style = LegadoTheme.typography.bodySmall,
+                        maxLines = 1,
+                        overflow = TextOverflow.Ellipsis,
+                        modifier = Modifier.fillMaxWidth()
+                    )
+                }
+                Box(modifier = Modifier.fillMaxWidth()) {
+                    innerTextField()
+                }
+            }
+        }
+    )
+}

+ 55 - 0
app/src/main/java/io/legado/app/utils/InfoMap.kt

@@ -0,0 +1,55 @@
+package io.legado.app.utils
+
+import androidx.annotation.Keep
+import io.legado.app.help.CacheManager
+
+/**
+ * 发现按钮信息
+ */
+@Keep
+class InfoMap(val sourceUrl: String) : MutableMap<String, String> {
+    private var actualMap: MutableMap<String, String>
+    var needSave = false
+    private var saveTime = 0
+
+    init {
+        val cache = CacheManager.get("infoMap_$sourceUrl")
+        actualMap = GSON.fromJsonObject<MutableMap<String, String>>(cache).getOrNull() ?: mutableMapOf()
+    }
+
+    /**
+     * time 保存时间 单位为秒
+     */
+    @JvmOverloads
+    fun save(time: Int = 0, need: Boolean = true) {
+        needSave = need
+        saveTime = time
+    }
+
+    fun saveNow() {
+        val json = GSON.toJson(actualMap)
+        CacheManager.put("infoMap_$sourceUrl", json, saveTime)
+        needSave = false
+    }
+
+    fun get(): MutableMap<String, String> {
+        return actualMap
+    }
+
+    fun set(value: Map<String, String>) {
+        actualMap = value.toMutableMap()
+    }
+
+    override fun get(key: String) = actualMap[key]
+    override fun put(key: String, value: String) = actualMap.put(key, value)
+    override fun remove(key: String) = actualMap.remove(key)
+    override fun putAll(from: Map<out String, String>) = actualMap.putAll(from)
+    override fun containsKey(key: String) = actualMap.containsKey(key)
+    override fun containsValue(value: String) = actualMap.containsValue(value)
+    override val size get() = actualMap.size
+    override val entries get() = actualMap.entries
+    override val keys get() = actualMap.keys
+    override val values get() = actualMap.values
+    override fun isEmpty() = actualMap.isEmpty()
+    override fun clear() = actualMap.clear()
+}