Просмотр исходного кода

feat:文本选择菜单支持多行显示&隐藏菜单项目

fansangg 4 месяцев назад
Родитель
Сommit
b40224fc95

+ 83 - 40
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -72,6 +72,7 @@ import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 
 /**
@@ -92,6 +93,10 @@ class ReadBookController(
     var onUnhandledEffect: (ReadBookEffect) -> Unit = {}
     var onClose: (() -> Unit)? = null
 
+    init {
+        refreshActionMenuItems()
+    }
+
     // Page state — moved from Activity
     var pageChanged: Boolean = false
         private set
@@ -117,8 +122,12 @@ class ReadBookController(
     private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
     private val _textMenuState = MutableStateFlow<TextMenuState?>(null)
     val textMenuState = _textMenuState.asStateFlow()
+    private var textMenuRequestVersion = 0L
+    @Volatile
+    private var cachedActionMenuItems: List<ActionMenuItem>? = null
 
     fun dismissTextActionMenu() {
+        textMenuRequestVersion++
         _textMenuState.value = null
     }
     private val popupAction by lazy { PopupAction(activity) }
@@ -293,15 +302,30 @@ class ReadBookController(
 
     override fun showTextActionMenu() {
         val r = refs ?: return
-        _textMenuState.value = TextMenuState(
-            selectedText = selectedText,
-            startX = r.textMenuPosition.x.toInt(),
-            startTopY = r.textMenuPosition.y.toInt(),
-            startBottomY = r.cursorLeft.y.toInt() + r.cursorLeft.height,
-            endX = r.cursorRight.x.toInt(),
-            endBottomY = r.cursorRight.y.toInt() + r.cursorRight.height,
-            items = getActionMenuItems()
-        )
+        val text = selectedText
+        val startX = r.textMenuPosition.x.toInt()
+        val startTopY = r.textMenuPosition.y.toInt()
+        val startBottomY = r.cursorLeft.y.toInt() + r.cursorLeft.height
+        val endX = r.cursorRight.x.toInt()
+        val endBottomY = r.cursorRight.y.toInt() + r.cursorRight.height
+        val requestVersion = ++textMenuRequestVersion
+
+        activity.lifecycleScope.launch {
+            val items = getActionMenuItems()
+            val readView = refs?.readView
+            if (textMenuRequestVersion != requestVersion || readView?.isTextSelected != true) {
+                return@launch
+            }
+            _textMenuState.value = TextMenuState(
+                selectedText = text,
+                startX = startX,
+                startTopY = startTopY,
+                startBottomY = startBottomY,
+                endX = endX,
+                endBottomY = endBottomY,
+                items = items
+            )
+        }
     }
 
     override fun autoPageStop() {
@@ -630,7 +654,10 @@ class ReadBookController(
         refs?.readView?.cancelSelect()
     }
 
-    fun getActionMenuItems(): List<ActionMenuItem> {
+    suspend fun getActionMenuItems(): List<ActionMenuItem> = withContext(IO) {
+        val cached = cachedActionMenuItems
+        if (cached != null) return@withContext cached
+
         val items = mutableListOf<ActionMenuItem>()
         items.add(ActionMenuItem(R.id.menu_copy, activity.getString(android.R.string.copy)))
         items.add(ActionMenuItem(R.id.menu_share_str, activity.getString(R.string.share)))
@@ -645,7 +672,7 @@ class ReadBookController(
         items.add(ActionMenuItem(R.id.menu_search_content, activity.getString(R.string.search_content)))
 
         val thirdPartyItems = mutableListOf<ActionMenuItem>()
-        kotlin.runCatching {
+        runCatching {
             val pm = activity.packageManager
             val intent = Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain")
             val resolveInfos = pm.queryIntentActivities(intent, 0)
@@ -657,7 +684,7 @@ class ReadBookController(
                 
                 val title = resolveInfo.loadLabel(pm).toString()
                 val icon = if (ReadConfig.showSelectMenuIcon) {
-                    kotlin.runCatching { resolveInfo.loadIcon(pm) }.getOrNull()
+                    runCatching { resolveInfo.loadIcon(pm) }.getOrNull()
                 } else null
 
                 thirdPartyItems.add(ActionMenuItem(id = -1, title = title, iconDrawable = icon, intent = processIntent))
@@ -666,47 +693,59 @@ class ReadBookController(
 
         val allItems = items + thirdPartyItems
         val configStr = ReadConfig.textSelectMenuConfig
-        if (configStr.isEmpty()) {
-            return allItems
-        }
-
-        return kotlin.runCatching {
-            val savedConfigs = GSON.fromJsonObject<List<SelectionMenuConfigItem>>(configStr).getOrNull() ?: emptyList()
-            val savedMap = savedConfigs.associateBy { it.id }
-
-            val sortedItems = mutableListOf<ActionMenuItem>()
-            for (saved in savedConfigs) {
-                val found = allItems.find { it.uniqueId == saved.id }
-                if (found != null) {
-                    sortedItems.add(found.copy(enabled = saved.enabled))
+        val result = if (configStr.isEmpty()) {
+            allItems
+        } else {
+            runCatching {
+                val savedConfigs = GSON.fromJsonObject<List<SelectionMenuConfigItem>>(configStr).getOrNull() ?: emptyList()
+                val savedMap = savedConfigs.associateBy { it.id }
+
+                val sortedItems = mutableListOf<ActionMenuItem>()
+                for (saved in savedConfigs) {
+                    val found = allItems.find { it.uniqueId == saved.id }
+                    if (found != null) {
+                        val resolvedShowState = saved.showState ?: if (saved.enabled == false) 1 else 0
+                        sortedItems.add(found.copy(showState = resolvedShowState))
+                    }
                 }
-            }
-            for (item in allItems) {
-                val uniqueId = item.uniqueId
-                if (!savedMap.containsKey(uniqueId)) {
-                    sortedItems.add(item)
+                for (item in allItems) {
+                    val uniqueId = item.uniqueId
+                    if (!savedMap.containsKey(uniqueId)) {
+                        sortedItems.add(item)
+                    }
                 }
+                sortedItems
+            }.getOrDefault(allItems)
+        }
+        cachedActionMenuItems = result
+        result
+    }
+
+    fun refreshActionMenuItems() {
+        cachedActionMenuItems = null
+        activity.lifecycleScope.launch {
+            val menuItems = getActionMenuItems()
+            _textMenuState.value?.let { currentState ->
+                _textMenuState.value = currentState.copy(items = menuItems)
             }
-            sortedItems
-        }.getOrDefault(allItems)
+        }
     }
 
     fun saveMenuConfig(items: List<ActionMenuItem>) {
         val configs = items.map { item ->
             SelectionMenuConfigItem(
                 id = item.uniqueId,
-                enabled = item.enabled
+                enabled = item.showState == 0,
+                showState = item.showState
             )
         }
         ReadConfig.textSelectMenuConfig = GSON.toJson(configs)
-        _textMenuState.value?.let { currentState ->
-            _textMenuState.value = currentState.copy(items = getActionMenuItems())
-        }
+        refreshActionMenuItems()
     }
 
     fun onTextMenuItemClick(item: ActionMenuItem) {
         if (item.intent != null) {
-            kotlin.runCatching {
+            runCatching {
                 item.intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText)
                 activity.startActivity(item.intent)
             }.onFailure { e ->
@@ -717,7 +756,7 @@ class ReadBookController(
                 R.id.menu_copy -> activity.sendToClip(selectedText)
                 R.id.menu_share_str -> activity.share(selectedText)
                 R.id.menu_browser -> {
-                    kotlin.runCatching {
+                    runCatching {
                         val intent = if (selectedText.isAbsUrl()) {
                             Intent(Intent.ACTION_VIEW).apply {
                                 data = selectedText.toUri()
@@ -1386,8 +1425,11 @@ data class ActionMenuItem(
     val title: String,
     val iconDrawable: android.graphics.drawable.Drawable? = null,
     val intent: Intent? = null,
-    val enabled: Boolean = true
+    val showState: Int = 0 // 0: 一级, 1: 折叠, 2: 隐藏
 ) {
+    val enabled: Boolean
+        get() = showState == 0
+
     val uniqueId: String
         get() = if (intent != null) {
             val comp = intent.component
@@ -1412,5 +1454,6 @@ data class ActionMenuItem(
 
 data class SelectionMenuConfigItem(
     val id: String,
-    val enabled: Boolean
+    val enabled: Boolean? = null,
+    val showState: Int? = null
 )

+ 6 - 3
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -489,6 +489,7 @@ fun ReadBookRouteScreen(
             textMenuState?.let { menuState ->
                 TextActionSelectionMenu(
                     menuState = menuState,
+                    expandTextMenu = readPreferences.expandTextMenu,
                     onDismiss = { controller.dismissTextActionMenu() },
                     onItemClick = { item -> controller.onTextMenuItemClick(item) },
                     onOpenManage = {
@@ -498,11 +499,12 @@ fun ReadBookRouteScreen(
                     }
                 )
             }
-            val configItems = remember(showSelectMenuConfigSheet) {
+            var configItems by remember { mutableStateOf<List<ActionMenuItem>>(emptyList()) }
+            LaunchedEffect(showSelectMenuConfigSheet) {
                 if (showSelectMenuConfigSheet) {
-                    controller.getActionMenuItems()
+                    configItems = controller.getActionMenuItems()
                 } else {
-                    emptyList()
+                    configItems = emptyList()
                 }
             }
             TextSelectMenuConfigSheet(
@@ -515,6 +517,7 @@ fun ReadBookRouteScreen(
                 },
                 onShowSelectMenuIconChange = {
                     viewModel.onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowSelectMenuIcon(it)))
+                    controller.refreshActionMenuItems()
                 },
                 onDismissRequest = { showSelectMenuConfigSheet = false },
                 onSaved = { items -> controller.saveMenuConfig(items) }

+ 112 - 50
app/src/main/java/io/legado/app/ui/book/read/TextActionSelectionMenu.kt

@@ -4,8 +4,10 @@ import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.heightIn
@@ -13,7 +15,6 @@ import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.layout.wrapContentWidth
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
@@ -21,7 +22,7 @@ import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material.icons.filled.MoreHoriz
+import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material3.Icon
 import androidx.compose.material3.VerticalDivider
@@ -33,6 +34,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
@@ -57,14 +59,23 @@ enum class MenuMode {
 @Composable
 fun TextActionSelectionMenu(
     menuState: TextMenuState,
+    expandTextMenu: Boolean,
     onDismiss: () -> Unit,
     onItemClick: (ActionMenuItem) -> Unit,
     onOpenManage: () -> Unit
 ) {
+
+    val localDensity = LocalDensity.current
+    val containerSize = LocalWindowInfo.current.containerSize
+    val maxMenuWidth = with(localDensity){ containerSize.width.toDp() } - 32.dp
     var mode by remember { mutableStateOf(MenuMode.Quick) }
     val draftItems = menuState.items
+    
+    val primaryItems = remember(draftItems) { draftItems.filter { it.showState == 0 } }
+    val collapsedItems = remember(draftItems) { draftItems.filter { it.showState == 1 } }
+    val activeMultiItems = remember(draftItems) { draftItems.filter { it.showState == 0 || it.showState == 1 } }
 
-    val density = LocalDensity.current.density
+    val density = localDensity.density
     val positionProvider = remember(menuState, density) {
         TextMenuPositionProvider(
             density = density,
@@ -87,40 +98,95 @@ fun TextActionSelectionMenu(
     ) {
 
         NormalCard(
-            modifier = Modifier.widthIn(max = 350.dp).wrapContentHeight(),
+            modifier = Modifier.widthIn(max = maxMenuWidth),
             containerColor = LegadoTheme.colorScheme.surfaceBright,
             elevation = 6.dp,
-            cornerRadius = 12.dp
+            cornerRadius = 12.dp,
         ) {
-            if (mode == MenuMode.Quick) {
-                QuickMenuView(
-                    items = draftItems,
-                    onItemClick = onItemClick,
-                    onMoreClick = { mode = MenuMode.More }
-                )
-            } else {
-                MoreMenuView(
-                    items = draftItems,
+            if (expandTextMenu) {
+                MultiLineMenuView(
+                    items = activeMultiItems,
                     onItemClick = onItemClick,
-                    onBack = { mode = MenuMode.Quick },
                     onManageClick = {
                         onDismiss()
                         onOpenManage()
                     }
                 )
+            } else {
+                if (mode == MenuMode.Quick) {
+                    QuickMenuView(
+                        items = primaryItems,
+                        hasMore = collapsedItems.isNotEmpty(),
+                        onItemClick = onItemClick,
+                        onMoreClick = { mode = MenuMode.More },
+                        onSettingsClick = {
+                            onDismiss()
+                            onOpenManage()
+                        }
+                    )
+                } else {
+                    MoreMenuView(
+                        items = collapsedItems,
+                        onItemClick = onItemClick,
+                        onBack = { mode = MenuMode.Quick },
+                        onManageClick = {
+                            onDismiss()
+                            onOpenManage()
+                        }
+                    )
+                }
             }
         }
     }
 }
 
 @Composable
-private fun QuickMenuView(
+private fun MultiLineMenuView(
     items: List<ActionMenuItem>,
     onItemClick: (ActionMenuItem) -> Unit,
-    onMoreClick: () -> Unit
+    onManageClick: () -> Unit
 ) {
-    val enabledItems = remember(items) { items.filter { it.enabled } }
+    FlowRow(
+        modifier = Modifier
+            .padding(all = 8.dp)
+            .heightIn(max = 300.dp)
+            .verticalScroll(rememberScrollState()),
+        horizontalArrangement = Arrangement.spacedBy(4.dp),
+        verticalArrangement = Arrangement.spacedBy(4.dp)
+    ) {
+        items.forEach { item ->
+            QuickMenuItem(item = item, onClick = { onItemClick(item) })
+        }
 
+        Row(
+            modifier = Modifier
+                .clickable(onClick = onManageClick)
+                .padding(horizontal = 12.dp, vertical = 8.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Icon(
+                imageVector = Icons.Default.Settings,
+                contentDescription = stringResource(R.string.edit_menu_items),
+                tint = LegadoTheme.colorScheme.onSurface,
+                modifier = Modifier.size(16.dp)
+            )
+            Spacer(modifier = Modifier.width(4.dp))
+            AppText(
+                text = stringResource(R.string.edit_menu_items),
+                fontSize = 13.sp,
+            )
+        }
+    }
+}
+
+@Composable
+private fun QuickMenuView(
+    items: List<ActionMenuItem>,
+    hasMore: Boolean,
+    onItemClick: (ActionMenuItem) -> Unit,
+    onMoreClick: () -> Unit,
+    onSettingsClick: () -> Unit
+) {
     Row(
         modifier = Modifier
             .wrapContentWidth()
@@ -131,30 +197,31 @@ private fun QuickMenuView(
             modifier = Modifier
                 .weight(1f, fill = false)
                 .padding(vertical = 6.dp),
-            horizontalArrangement = Arrangement.spacedBy(4.dp),
             verticalAlignment = Alignment.CenterVertically,
         ) {
-            items(enabledItems, key = { it.uniqueId }) { item ->
+            items(items, key = { it.uniqueId }) { item ->
                 QuickMenuItem(item = item, onClick = { onItemClick(item) })
             }
         }
-        VerticalDivider(
-            color = LegadoTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
-            modifier = Modifier
-                .height(20.dp)
-                .width(1.dp)
-        )
+        if (items.isNotEmpty()) {
+            VerticalDivider(
+                color = LegadoTheme.colorScheme.outlineVariant.copy(alpha = 0.4f),
+                modifier = Modifier
+                    .height(20.dp)
+                    .width(1.dp)
+            )
+        }
         Box(
             modifier = Modifier
-                .clickable(onClick = onMoreClick)
-                .padding(horizontal = 12.dp, vertical = 8.dp),
+                .clickable(onClick = if (hasMore) onMoreClick else onSettingsClick)
+                .padding(horizontal = 10.dp, vertical = 8.dp),
             contentAlignment = Alignment.Center
         ) {
             Icon(
-                imageVector = Icons.Default.MoreHoriz,
-                contentDescription = stringResource(R.string.more_menu),
+                imageVector = if (hasMore) Icons.Default.MoreVert else Icons.Default.Settings,
+                contentDescription = stringResource(if (hasMore) R.string.more_menu else R.string.setting),
                 tint = LegadoTheme.colorScheme.onSurface,
-                modifier = Modifier.size(18.dp)
+                modifier = Modifier.size(16.dp)
             )
         }
     }
@@ -168,7 +235,7 @@ private fun QuickMenuItem(
     Row(
         modifier = Modifier
             .clickable(onClick = onClick)
-            .padding(horizontal = 12.dp, vertical = 8.dp),
+            .padding(horizontal = 10.dp, vertical = 8.dp),
         verticalAlignment = Alignment.CenterVertically
     ) {
         if (item.iconDrawable != null) {
@@ -177,13 +244,13 @@ private fun QuickMenuItem(
                 contentDescription = item.title,
                 modifier = Modifier
                     .size(16.dp)
-                    .padding(end = 4.dp)
             )
+
+            Spacer(modifier = Modifier.width(4.dp))
         }
         AppText(
             text = item.title,
             fontSize = 13.sp,
-            color = LegadoTheme.colorScheme.onSurface,
             maxLines = 1
         )
     }
@@ -196,25 +263,24 @@ private fun MoreMenuView(
     onBack: () -> Unit,
     onManageClick: () -> Unit
 ) {
-    val disabledItems = remember(items) { items.filter { !it.enabled } }
     val scrollState = rememberScrollState()
 
     Column(
         modifier = Modifier
             .width(IntrinsicSize.Max)
-            .padding(vertical = 4.dp)
+            .padding(vertical = 4.dp, horizontal = 10.dp)
     ) {
         Box(
             modifier = Modifier
                 .fillMaxWidth()
                 .clickable(onClick = onBack)
-                .padding(horizontal = 16.dp, vertical = 8.dp),
+                .padding(vertical = 8.dp),
             contentAlignment = Alignment.CenterStart
         ) {
             Icon(
                 imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                 contentDescription = stringResource(R.string.back),
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                tint = LegadoTheme.colorScheme.onSurface,
                 modifier = Modifier.size(18.dp)
             )
         }
@@ -225,7 +291,7 @@ private fun MoreMenuView(
                 .verticalScroll(scrollState)
                 .fillMaxWidth()
         ) {
-            disabledItems.forEach { item ->
+            items.forEach { item ->
                 MoreMenuItem(item = item, onClick = { onItemClick(item) })
             }
         }
@@ -234,22 +300,20 @@ private fun MoreMenuView(
             modifier = Modifier
                 .fillMaxWidth()
                 .clickable(onClick = onManageClick)
-                .padding(horizontal = 16.dp, vertical = 10.dp),
+                .padding(vertical = 8.dp),
             verticalAlignment = Alignment.CenterVertically
         ) {
             Icon(
                 imageVector = Icons.Default.Settings,
                 contentDescription = stringResource(R.string.edit_menu_items),
-                tint = LegadoTheme.colorScheme.primary,
+                tint = LegadoTheme.colorScheme.onSurface,
                 modifier = Modifier
-                    .size(18.dp)
-                    .padding(end = 8.dp)
+                    .size(16.dp)
             )
+            Spacer(modifier = Modifier.width(8.dp))
             AppText(
                 text = stringResource(R.string.edit_menu_items),
                 fontSize = 13.sp,
-                color = LegadoTheme.colorScheme.primary,
-                fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
             )
         }
     }
@@ -264,7 +328,7 @@ private fun MoreMenuItem(
         modifier = Modifier
             .fillMaxWidth()
             .clickable(onClick = onClick)
-            .padding(horizontal = 16.dp, vertical = 10.dp),
+            .padding(vertical = 8.dp),
         verticalAlignment = Alignment.CenterVertically
     ) {
         if (item.iconDrawable != null) {
@@ -272,15 +336,13 @@ private fun MoreMenuItem(
                 model = item.iconDrawable,
                 contentDescription = item.title,
                 modifier = Modifier
-                    .size(18.dp)
-                    .padding(end = 8.dp)
+                    .size(16.dp)
             )
+            Spacer(modifier = Modifier.width(8.dp))
         }
         AppText(
             text = item.title,
             fontSize = 13.sp,
-            color = LegadoTheme.colorScheme.onSurface,
-            maxLines = 1
         )
     }
 }

+ 404 - 89
app/src/main/java/io/legado/app/ui/book/read/sheet/TextSelectMenuConfigSheet.kt

@@ -1,31 +1,45 @@
 package io.legado.app.ui.book.read.sheet
 
 import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.KeyboardArrowDown
 import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.Remove
 import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 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.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
@@ -38,11 +52,16 @@ import io.legado.app.ui.widget.components.TinySwitch
 import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinySettingItem
 import io.legado.app.ui.widget.components.text.AppText
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import sh.calvin.reorderable.ReorderableItem
 import sh.calvin.reorderable.rememberReorderableLazyListState
+import kotlin.time.Duration.Companion.milliseconds
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
 @Composable
 fun TextSelectMenuConfigSheet(
     show: Boolean,
@@ -58,10 +77,37 @@ fun TextSelectMenuConfigSheet(
         mutableStateOf(items)
     }
 
+    var group1Expanded by remember(show) { mutableStateOf(true) }
+    var group2Expanded by remember(show) { mutableStateOf(true) }
+    var group3Expanded by remember(show) { mutableStateOf(false) }
+
+    val group1Items = remember(draftItems) { draftItems.filter { it.showState == 0 } }
+    val group2Items = remember(draftItems) { draftItems.filter { it.showState == 1 } }
+    val group3Items = remember(draftItems) { draftItems.filter { it.showState == 2 } }
+
     val lazyListState = rememberLazyListState()
+    val coroutineScope = rememberCoroutineScope()
+
+    LaunchedEffect(show) {
+        if (show) {
+            lazyListState.scrollToItem(0)
+        }
+    }
     val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
-        draftItems = draftItems.toMutableList().apply {
-            add(to.index, removeAt(from.index))
+        val fromKey = from.key
+        val toKey = to.key
+
+        val fromIndexInDraft = draftItems.indexOfFirst { it.uniqueId == fromKey }
+        val toIndexInDraft = draftItems.indexOfFirst { it.uniqueId == toKey }
+
+        if (fromIndexInDraft != -1 && toIndexInDraft != -1) {
+            val fromItem = draftItems[fromIndexInDraft]
+            val toItem = draftItems[toIndexInDraft]
+            if (fromItem.showState == toItem.showState) {
+                draftItems = draftItems.toMutableList().apply {
+                    add(toIndexInDraft, removeAt(fromIndexInDraft))
+                }
+            }
         }
     }
 
@@ -83,85 +129,186 @@ fun TextSelectMenuConfigSheet(
                     .weight(1f)
                     .padding(horizontal = 16.dp),
             ) {
-                items(draftItems, key = { it.uniqueId }) { item ->
-                    ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
-                        val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
-                        NormalCard(
-                            elevation = elevation,
-                            cornerRadius = 12.dp,
-                            containerColor = LegadoTheme.colorScheme.surfaceContainerLow
-                        ) {
-                            TextSelectMenuConfigItemRow(
-                                item = item,
-                                onToggleEnabled = {
-                                    draftItems = draftItems.map {
-                                        if (it.uniqueId == item.uniqueId) {
-                                            it.copy(enabled = !it.enabled)
-                                        } else it
-                                    }
-                                },
-                                dragHandleModifier = Modifier.draggableHandle()
+                item {
+                    NormalCard(
+                        cornerRadius = 12.dp,
+                        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .animateItem()
+                            .padding(top = 12.dp, bottom = 4.dp)
+                    ) {
+                        Column {
+                            Row(
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .clickable { onExpandTextMenuChange(!expandTextMenu) }
+                                    .padding(horizontal = 16.dp, vertical = 12.dp),
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                AppText(
+                                    text = stringResource(R.string.expand_text_menu),
+                                    style = LegadoTheme.typography.titleSmallEmphasized,
+                                    modifier = Modifier.weight(1f),
+                                    maxLines = 1,
+                                    overflow = TextOverflow.Ellipsis
+                                )
+                                TinySwitch(
+                                    checked = expandTextMenu,
+                                    onCheckedChange = onExpandTextMenuChange
+                                )
+                            }
+
+                            HorizontalDivider(
+                                modifier = Modifier.padding(horizontal = 16.dp),
+                                color = LegadoTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)
                             )
+
+                            Row(
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .clickable { onShowSelectMenuIconChange(!showSelectMenuIcon) }
+                                    .padding(horizontal = 16.dp, vertical = 12.dp),
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                AppText(
+                                    text = stringResource(R.string.show_select_menu_icon),
+                                    style = LegadoTheme.typography.titleSmallEmphasized,
+                                    modifier = Modifier.weight(1f),
+                                    maxLines = 1,
+                                    overflow = TextOverflow.Ellipsis
+                                )
+                                TinySwitch(
+                                    checked = showSelectMenuIcon,
+                                    onCheckedChange = onShowSelectMenuIconChange
+                                )
+                            }
                         }
                     }
                 }
-            }
 
-            Row(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 4.dp),
-                horizontalArrangement = Arrangement.spacedBy(8.dp),
-                verticalAlignment = Alignment.CenterVertically
-            ) {
-                NormalCard(
-                    cornerRadius = 12.dp,
-                    containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                    modifier = Modifier.weight(1f),
-                    onClick = { onExpandTextMenuChange(!expandTextMenu) }
-                ) {
-                    Row(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(horizontal = 12.dp, vertical = 12.dp),
-                        verticalAlignment = Alignment.CenterVertically
-                    ) {
-                        AppText(
-                            text = stringResource(R.string.expand_text_menu),
-                            style = LegadoTheme.typography.titleSmallEmphasized,
-                            modifier = Modifier.weight(1f),
-                            maxLines = 1,
-                            overflow = TextOverflow.Ellipsis
-                        )
-                        TinySwitch(
-                            checked = expandTextMenu,
-                            onCheckedChange = onExpandTextMenuChange
-                        )
+                stickyHeader(key = "group1") {
+                    SheetHeader(
+                        isCollapsed = !group1Expanded,
+                        onToggle = { group1Expanded = !group1Expanded },
+                        title = stringResource(R.string.primary_menu)
+                    )
+                }
+
+                if (group1Expanded) {
+                    items(group1Items, key = { it.uniqueId }) { item ->
+                        ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
+                            val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
+                            NormalCard(
+                                elevation = elevation,
+                                cornerRadius = 12.dp,
+                                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                                modifier = Modifier.animateItem()
+                            ) {
+                                PrimaryMenuItemRow(
+                                    item = item,
+                                    showIcon = showSelectMenuIcon,
+                                    onMoveToFolded = {
+                                        draftItems = draftItems.map {
+                                            if (it.uniqueId == item.uniqueId) it.copy(showState = 1) else it
+                                        }
+                                    },
+                                    onMoveToHidden = {
+                                        draftItems = draftItems.map {
+                                            if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
+                                        }
+                                    },
+                                    dragHandleModifier = Modifier.draggableHandle()
+                                )
+                            }
+                        }
                     }
                 }
 
-                NormalCard(
-                    cornerRadius = 12.dp,
-                    containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                    modifier = Modifier.weight(1f),
-                    onClick = { onShowSelectMenuIconChange(!showSelectMenuIcon) }
-                ) {
-                    Row(
+
+                stickyHeader(key = "group2") {
+                    val subtitle = if (expandTextMenu) stringResource(R.string.merged_in_multiline) else null
+                    SheetHeader(
+                        isCollapsed = !group2Expanded,
+                        onToggle = { group2Expanded = !group2Expanded },
+                        title = stringResource(R.string.collapsed_menu),
+                        subtitle = subtitle
+                    )
+                }
+
+                if (group2Expanded) {
+                    items(group2Items, key = { it.uniqueId }) { item ->
+                        ReorderableItem(reorderableState, key = item.uniqueId) { isDragging ->
+                            val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
+                            NormalCard(
+                                elevation = elevation,
+                                cornerRadius = 12.dp,
+                                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                                modifier = Modifier.animateItem()
+                            ) {
+                                FoldedMenuItemRow(
+                                    item = item,
+                                    showIcon = showSelectMenuIcon,
+                                    onMoveToPrimary = {
+                                        draftItems = draftItems.map {
+                                            if (it.uniqueId == item.uniqueId) it.copy(showState = 0) else it
+                                        }
+                                    },
+                                    onMoveToHidden = {
+                                        draftItems = draftItems.map {
+                                            if (it.uniqueId == item.uniqueId) it.copy(showState = 2) else it
+                                        }
+                                    },
+                                    dragHandleModifier = Modifier.draggableHandle()
+                                )
+                            }
+                        }
+                    }
+                }
+
+
+                item (key="group_3"){
+                    Box(
                         modifier = Modifier
                             .fillMaxWidth()
-                            .padding(horizontal = 12.dp, vertical = 12.dp),
-                        verticalAlignment = Alignment.CenterVertically
+                            .animateItem()
+                            .background(LegadoTheme.colorScheme.surfaceContainer)
+                            .padding(top = 12.dp)
                     ) {
-                        AppText(
-                            text = stringResource(R.string.show_select_menu_icon),
-                            style = LegadoTheme.typography.titleSmallEmphasized,
-                            modifier = Modifier.weight(1f),
-                            maxLines = 1,
-                            overflow = TextOverflow.Ellipsis
-                        )
-                        TinySwitch(
-                            checked = showSelectMenuIcon,
-                            onCheckedChange = onShowSelectMenuIconChange
+                        TinySettingItem(
+                            title = stringResource(R.string.hidden_items),
+                            color = LegadoTheme.colorScheme.surfaceContainerHigh,
+                            expanded = group3Expanded,
+                            onExpandChange = { expanded ->
+                                group3Expanded = expanded
+                                if (expanded) {
+                                    coroutineScope.launch {
+                                        delay(300.milliseconds)
+                                        val targetIndex = lazyListState.layoutInfo.totalItemsCount - 1
+                                        if (targetIndex >= 0) {
+                                            lazyListState.animateScrollToItem(targetIndex)
+                                        }
+                                    }
+                                }
+                            },
+                            expandContent = {
+                                HiddenItemsFlowView(
+                                    items = group3Items,
+                                    showIcon = showSelectMenuIcon,
+                                    onRestore = { item ->
+                                        draftItems = draftItems.map {
+                                            if (it.uniqueId == item.uniqueId) it.copy(showState = 0) else it
+                                        }
+                                        coroutineScope.launch {
+                                            delay(200.milliseconds)
+                                            val targetIndex = lazyListState.layoutInfo.totalItemsCount - 1
+                                            if (targetIndex >= 0) {
+                                                lazyListState.scrollToItem(targetIndex)
+                                            }
+                                        }
+                                    }
+                                )
+                            }
                         )
                     }
                 }
@@ -182,19 +329,20 @@ fun TextSelectMenuConfigSheet(
 }
 
 @Composable
-private fun TextSelectMenuConfigItemRow(
+private fun PrimaryMenuItemRow(
     item: ActionMenuItem,
-    onToggleEnabled: () -> Unit,
+    showIcon: Boolean,
+    onMoveToFolded: () -> Unit,
+    onMoveToHidden: () -> Unit,
     dragHandleModifier: Modifier = Modifier
 ) {
-    val alpha = if (item.enabled) 1f else 0.38f
     Row(
         verticalAlignment = Alignment.CenterVertically,
         modifier = Modifier
             .fillMaxWidth()
             .padding(all = 12.dp),
     ) {
-        if (item.iconDrawable != null) {
+        if (showIcon && item.iconDrawable != null) {
             Box(
                 contentAlignment = Alignment.Center,
                 modifier = Modifier.size(36.dp),
@@ -202,7 +350,6 @@ private fun TextSelectMenuConfigItemRow(
                 AsyncImage(
                     model = item.iconDrawable,
                     contentDescription = item.title,
-                    alpha = alpha,
                     modifier = Modifier.size(24.dp)
                 )
             }
@@ -211,7 +358,7 @@ private fun TextSelectMenuConfigItemRow(
         AppText(
             text = item.title,
             fontSize = 14.sp,
-            color = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
+            color = LegadoTheme.colorScheme.onSurface,
             modifier = Modifier
                 .weight(1f)
                 .padding(horizontal = 12.dp),
@@ -219,21 +366,100 @@ private fun TextSelectMenuConfigItemRow(
         )
 
         IconButton(
-            onClick = onToggleEnabled,
+            onClick = onMoveToFolded,
             modifier = Modifier.size(36.dp),
         ) {
             Icon(
-                imageVector = if (item.enabled) {
-                    Icons.Default.Visibility
-                } else {
-                    Icons.Default.VisibilityOff
-                },
+                imageVector = Icons.Default.Remove,
                 contentDescription = null,
-                tint = if (item.enabled) {
-                    LegadoTheme.colorScheme.onSurface
-                } else {
-                    LegadoTheme.colorScheme.onSurfaceVariant
-                },
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                modifier = Modifier.size(20.dp),
+            )
+        }
+
+        IconButton(
+            onClick = onMoveToHidden,
+            modifier = Modifier.size(36.dp),
+        ) {
+            Icon(
+                imageVector = Icons.Default.VisibilityOff,
+                contentDescription = null,
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                modifier = Modifier.size(20.dp),
+            )
+        }
+
+        Box(
+            modifier = dragHandleModifier.size(36.dp),
+            contentAlignment = Alignment.Center
+        ) {
+            Icon(
+                imageVector = Icons.Default.Menu,
+                contentDescription = null,
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                modifier = Modifier.size(20.dp),
+            )
+        }
+    }
+}
+
+@Composable
+private fun FoldedMenuItemRow(
+    item: ActionMenuItem,
+    showIcon: Boolean,
+    onMoveToPrimary: () -> Unit,
+    onMoveToHidden: () -> Unit,
+    dragHandleModifier: Modifier = Modifier
+) {
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(all = 12.dp),
+    ) {
+        if (showIcon && item.iconDrawable != null) {
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier.size(36.dp),
+            ) {
+                AsyncImage(
+                    model = item.iconDrawable,
+                    contentDescription = item.title,
+                    modifier = Modifier.size(24.dp)
+                )
+            }
+        }
+
+        AppText(
+            text = item.title,
+            fontSize = 14.sp,
+            color = LegadoTheme.colorScheme.onSurface,
+            modifier = Modifier
+                .weight(1f)
+                .padding(horizontal = 12.dp),
+            maxLines = 1
+        )
+
+        IconButton(
+            onClick = onMoveToPrimary,
+            modifier = Modifier.size(36.dp),
+        ) {
+            Icon(
+                imageVector = Icons.Default.Add,
+                contentDescription = null,
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                modifier = Modifier.size(20.dp),
+            )
+        }
+
+        IconButton(
+            onClick = onMoveToHidden,
+            modifier = Modifier.size(36.dp),
+        ) {
+            Icon(
+                imageVector = Icons.Default.VisibilityOff,
+                contentDescription = null,
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
                 modifier = Modifier.size(20.dp),
             )
         }
@@ -251,3 +477,92 @@ private fun TextSelectMenuConfigItemRow(
         }
     }
 }
+
+@Composable
+private fun HiddenItemsFlowView(
+    items: List<ActionMenuItem>,
+    showIcon: Boolean,
+    modifier: Modifier = Modifier,
+    onRestore: (ActionMenuItem) -> Unit
+) {
+    FlowRow(
+        modifier = modifier
+            .fillMaxWidth()
+            .padding(vertical = 8.dp),
+        horizontalArrangement = Arrangement.spacedBy(8.dp),
+        verticalArrangement = Arrangement.spacedBy(8.dp)
+    ) {
+        items.forEach { item ->
+            NormalCard(
+                onClick = { onRestore(item) },
+                cornerRadius = 8.dp,
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+            ) {
+                Row(
+                    verticalAlignment = Alignment.CenterVertically,
+                    modifier = Modifier.padding(start = 8.dp, end = 4.dp, top = 4.dp, bottom = 4.dp)
+                ) {
+                    if (showIcon && item.iconDrawable != null) {
+                        AsyncImage(
+                            model = item.iconDrawable,
+                            contentDescription = item.title,
+                            modifier = Modifier
+                                .size(16.dp)
+                        )
+                        Spacer(modifier = Modifier.width(4.dp))
+                    }
+
+                    AppText(
+                        text = item.title,
+                        fontSize = 12.sp,
+                    )
+
+                    Icon(
+                        imageVector = Icons.Default.Add,
+                        contentDescription = null,
+                        tint = LegadoTheme.colorScheme.primary,
+                        modifier = Modifier.size(16.dp)
+                    )
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun LazyItemScope.SheetHeader(
+    title: String,
+    subtitle: String? = null,
+    isCollapsed: Boolean,
+    onToggle: () -> Unit
+) {
+    val rotation by animateFloatAsState(
+        targetValue = if (!isCollapsed) 180f else 0f,
+        label = "sheetHeaderArrow"
+    )
+    Box(
+        modifier = Modifier
+            .fillMaxWidth()
+            .animateItem()
+            .background(LegadoTheme.colorScheme.surfaceContainer)
+            .padding(top = 12.dp)
+    ) {
+        TinyClickableSettingItem(
+            title = title,
+            description = subtitle,
+            color = LegadoTheme.colorScheme.surfaceContainerHigh,
+            trailingContent = {
+                Icon(
+                    imageVector = Icons.Default.KeyboardArrowDown,
+                    contentDescription = null,
+                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                    modifier = Modifier
+                        .size(20.dp)
+                        .rotate(rotation),
+                )
+            },
+            onClick = onToggle
+        )
+    }
+}

+ 4 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -2456,4 +2456,8 @@
     <string name="about_open_github">打开 GitHub</string>
     <string name="edit_menu_items">编辑菜单项</string>
     <string name="edit_select_menu">编辑选择菜单</string>
+    <string name="primary_menu">一级菜单</string>
+    <string name="collapsed_menu">折叠菜单</string>
+    <string name="hidden_items">已隐藏项目</string>
+    <string name="merged_in_multiline">多行模式下将合并展示</string>
 </resources>

+ 4 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -2368,4 +2368,8 @@
     <string name="about_open_github">開啟 GitHub</string>
     <string name="edit_menu_items">編輯選單項</string>
     <string name="edit_select_menu">編輯選擇選單</string>
+    <string name="primary_menu">一級選單</string>
+    <string name="collapsed_menu">折疊選單</string>
+    <string name="hidden_items">已隱藏項目</string>
+    <string name="merged_in_multiline">多行模式下將合併顯示</string>
 </resources>

+ 4 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -2373,4 +2373,8 @@
     <string name="about_open_github">開啟 GitHub</string>
     <string name="edit_menu_items">編輯選單項</string>
     <string name="edit_select_menu">編輯選擇選單</string>
+    <string name="primary_menu">一級選單</string>
+    <string name="collapsed_menu">折疊選單</string>
+    <string name="hidden_items">已隱藏項目</string>
+    <string name="merged_in_multiline">多行模式下將合併顯示</string>
 </resources>

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -2461,4 +2461,8 @@
     <string name="about_open_github">Open GitHub</string>
     <string name="edit_menu_items">Edit Menu Items</string>
     <string name="edit_select_menu">Edit Selection Menu</string>
+    <string name="primary_menu">Primary Menu</string>
+    <string name="collapsed_menu">Collapsed Menu</string>
+    <string name="hidden_items">Hidden Items</string>
+    <string name="merged_in_multiline">Merged in multiline mode</string>
 </resources>