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

Merge pull request #1712 from fansangg/feature/compose-selection-menu

refactor:重构文字选择菜单为Compose实现,并支持菜单管理
Kudomaga 4 месяцев назад
Родитель
Сommit
2bbd3c4d22

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

@@ -136,6 +136,7 @@ object PreferKey {
     const val expandTextMenu = "expandTextMenu"
     const val expandTextMenu = "expandTextMenu"
     const val showSelectMenuIcon = "showSelectMenuIcon"
     const val showSelectMenuIcon = "showSelectMenuIcon"
     const val textSelectMenuFilter = "textSelectMenuFilter"
     const val textSelectMenuFilter = "textSelectMenuFilter"
+    const val textSelectMenuConfig = "textSelectMenuConfig"
     const val doublePageHorizontal = "doubleHorizontalPage"
     const val doublePageHorizontal = "doubleHorizontalPage"
     const val readUrlOpenInBrowser = "readUrlInBrowser"
     const val readUrlOpenInBrowser = "readUrlInBrowser"
     const val defaultBookTreeUri = "defaultBookTreeUri"
     const val defaultBookTreeUri = "defaultBookTreeUri"

+ 192 - 30
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -60,10 +60,21 @@ import io.legado.app.utils.sysScreenOffTime
 import io.legado.app.utils.throttle
 import io.legado.app.utils.throttle
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.visible
 import io.legado.app.utils.visible
+import io.legado.app.utils.sendToClip
+import io.legado.app.utils.share
+import io.legado.app.utils.isAbsUrl
+import io.legado.app.utils.printOnDebug
+import android.app.SearchManager
+import android.content.Context
+import android.content.Intent
+import androidx.core.net.toUri
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
+
 /**
 /**
  * Encapsulates all the reader logic that used to be in ReadBookActivity.
  * Encapsulates all the reader logic that used to be in ReadBookActivity.
  * This allows ReadBookRouteScreen to be hosted in any Activity (ReadBookActivity or MainActivity).
  * This allows ReadBookRouteScreen to be hosted in any Activity (ReadBookActivity or MainActivity).
@@ -74,8 +85,7 @@ class ReadBookController(
 ) : ReadBookRouteHost,
 ) : ReadBookRouteHost,
     ReadBookInputHandler,
     ReadBookInputHandler,
     ReadView.CallBack,
     ReadView.CallBack,
-    ContentTextView.CallBack,
-    TextActionMenu.CallBack {
+    ContentTextView.CallBack {
 
 
     var refs: ReadBookViewRefs? = null
     var refs: ReadBookViewRefs? = null
 
 
@@ -106,12 +116,11 @@ class ReadBookController(
     private val networkChangedListener by lazy { NetworkChangedListener(activity) }
     private val networkChangedListener by lazy { NetworkChangedListener(activity) }
     private val handler by lazy { buildMainHandler() }
     private val handler by lazy { buildMainHandler() }
     private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
     private val screenOffRunnable by lazy { Runnable { keepScreenOn(false) } }
-    private val textActionMenu by lazy {
-        TextActionMenu(
-            context = activity,
-            callBack = this,
-            expandTextMenu = { viewModel.readPreferences.value.expandTextMenu }
-        )
+    private val _textMenuState = MutableStateFlow<TextMenuState?>(null)
+    val textMenuState = _textMenuState.asStateFlow()
+
+    fun dismissTextActionMenu() {
+        _textMenuState.value = null
     }
     }
     private val popupAction by lazy { PopupAction(activity) }
     private val popupAction by lazy { PopupAction(activity) }
     private var screenTimeOut: Long = 0
     private var screenTimeOut: Long = 0
@@ -134,7 +143,7 @@ class ReadBookController(
     fun clearTts() {
     fun clearTts() {
         tts?.clearTts()
         tts?.clearTts()
         tts = null
         tts = null
-        textActionMenu.dismiss()
+        dismissTextActionMenu()
         popupAction.dismiss()
         popupAction.dismiss()
         refs?.readView?.onDestroy()
         refs?.readView?.onDestroy()
         networkChangedListener.unRegister()
         networkChangedListener.unRegister()
@@ -285,21 +294,14 @@ class ReadBookController(
 
 
     override fun showTextActionMenu() {
     override fun showTextActionMenu() {
         val r = refs ?: return
         val r = refs ?: return
-        val navigationBarHeight =
-            if (!ReadBookConfig.hideNavigationBar && activity.navigationBarGravity == Gravity.BOTTOM) {
-                r.navigationBar.height
-            } else {
-                0
-            }
-        textActionMenu.upMenu()
-        textActionMenu.show(
-            r.textMenuPosition,
-            r.root.height + navigationBarHeight,
-            r.textMenuPosition.x.toInt(),
-            r.textMenuPosition.y.toInt(),
-            r.cursorLeft.y.toInt() + r.cursorLeft.height,
-            r.cursorRight.x.toInt(),
-            r.cursorRight.y.toInt() + r.cursorRight.height
+        _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()
         )
         )
     }
     }
 
 
@@ -415,7 +417,7 @@ class ReadBookController(
     override fun onCancelSelect() {
     override fun onCancelSelect() {
         refs?.cursorLeft?.invisible()
         refs?.cursorLeft?.invisible()
         refs?.cursorRight?.invisible()
         refs?.cursorRight?.invisible()
-        textActionMenu.dismiss()
+        dismissTextActionMenu()
     }
     }
 
 
     override fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean =
     override fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean =
@@ -505,7 +507,7 @@ class ReadBookController(
             return false
             return false
         }
         }
         when (event.action) {
         when (event.action) {
-            MotionEvent.ACTION_DOWN -> textActionMenu.dismiss()
+            MotionEvent.ACTION_DOWN -> dismissTextActionMenu()
             MotionEvent.ACTION_MOVE -> {
             MotionEvent.ACTION_MOVE -> {
                 when (v.id) {
                 when (v.id) {
                     R.id.cursor_left -> if (!r.readView.curPage.getReverseStartCursor()) {
                     R.id.cursor_left -> if (!r.readView.curPage.getReverseStartCursor()) {
@@ -542,9 +544,9 @@ class ReadBookController(
         return true
         return true
     }
     }
 
 
-    override val selectedText: String get() = refs?.readView?.getSelectText().orEmpty()
+    val selectedText: String get() = refs?.readView?.getSelectText().orEmpty()
 
 
-    override fun onMenuItemSelected(itemId: Int): Boolean {
+    fun onMenuItemSelected(itemId: Int): Boolean {
         when (itemId) {
         when (itemId) {
             R.id.menu_aloud -> {
             R.id.menu_aloud -> {
                 viewModel.onIntent(
                 viewModel.onIntent(
@@ -612,11 +614,127 @@ class ReadBookController(
         return false
         return false
     }
     }
 
 
-    override fun onMenuActionFinally() {
-        textActionMenu.dismiss()
+    fun onMenuActionFinally() {
+        dismissTextActionMenu()
         refs?.readView?.cancelSelect()
         refs?.readView?.cancelSelect()
     }
     }
 
 
+    fun getActionMenuItems(): List<ActionMenuItem> {
+        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)))
+        items.add(ActionMenuItem(R.id.menu_browser, activity.getString(R.string.browser)))
+        items.add(ActionMenuItem(R.id.menu_aloud, activity.getString(R.string.read_aloud)))
+        items.add(ActionMenuItem(R.id.menu_bookmark, activity.getString(R.string.bookmark)))
+        items.add(ActionMenuItem(R.id.menu_dict, activity.getString(R.string.dict)))
+        items.add(ActionMenuItem(R.id.menu_replace, activity.getString(R.string.replace)))
+        items.add(ActionMenuItem(R.id.menu_edit, activity.getString(R.string.edit)))
+        items.add(ActionMenuItem(R.id.menu_ai_clean, activity.getString(R.string.ai_text_clean)))
+        items.add(ActionMenuItem(R.id.menu_ai_rewrite, activity.getString(R.string.ai_text_rewrite)))
+        items.add(ActionMenuItem(R.id.menu_search_content, activity.getString(R.string.search_content)))
+
+        val thirdPartyItems = mutableListOf<ActionMenuItem>()
+        kotlin.runCatching {
+            val pm = activity.packageManager
+            val filterSet = ReadConfig.textSelectMenuFilter.split(",").filter { it.isNotEmpty() }.toSet()
+            val intent = Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain")
+            val resolveInfos = pm.queryIntentActivities(intent, 0)
+            for (resolveInfo in resolveInfos) {
+                val componentName = "${resolveInfo.activityInfo.packageName}/${resolveInfo.activityInfo.name}"
+                if (filterSet.contains(componentName)) {
+                    continue
+                }
+                val processIntent = Intent()
+                    .setAction(Intent.ACTION_PROCESS_TEXT)
+                    .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false)
+                    .setClassName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name)
+                
+                val title = resolveInfo.loadLabel(pm).toString()
+                val icon = if (ReadConfig.showSelectMenuIcon) {
+                    kotlin.runCatching { resolveInfo.loadIcon(pm) }.getOrNull()
+                } else null
+
+                thirdPartyItems.add(ActionMenuItem(id = -1, title = title, iconDrawable = icon, intent = processIntent))
+            }
+        }
+
+        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))
+                }
+            }
+            for (item in allItems) {
+                val uniqueId = item.uniqueId
+                if (!savedMap.containsKey(uniqueId)) {
+                    sortedItems.add(item)
+                }
+            }
+            sortedItems
+        }.getOrDefault(allItems)
+    }
+
+    fun saveMenuConfig(items: List<ActionMenuItem>) {
+        val configs = items.map { item ->
+            SelectionMenuConfigItem(
+                id = item.uniqueId,
+                enabled = item.enabled
+            )
+        }
+        ReadConfig.textSelectMenuConfig = GSON.toJson(configs)
+        _textMenuState.value?.let { currentState ->
+            _textMenuState.value = currentState.copy(items = getActionMenuItems())
+        }
+    }
+
+    fun onTextMenuItemClick(item: ActionMenuItem) {
+        if (item.intent != null) {
+            kotlin.runCatching {
+                item.intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText)
+                activity.startActivity(item.intent)
+            }.onFailure { e ->
+                AppLog.put("执行文本菜单操作出错\n$e", e, true)
+            }
+        } else {
+            when (item.id) {
+                R.id.menu_copy -> activity.sendToClip(selectedText)
+                R.id.menu_share_str -> activity.share(selectedText)
+                R.id.menu_browser -> {
+                    kotlin.runCatching {
+                        val intent = if (selectedText.isAbsUrl()) {
+                            Intent(Intent.ACTION_VIEW).apply {
+                                data = selectedText.toUri()
+                            }
+                        } else {
+                            Intent(Intent.ACTION_WEB_SEARCH).apply {
+                                putExtra(SearchManager.QUERY, selectedText)
+                            }
+                        }
+                        activity.startActivity(intent)
+                    }.onFailure { e ->
+                        e.printOnDebug()
+                        activity.toastOnUi(e.localizedMessage ?: "ERROR")
+                    }
+                }
+                else -> {
+                    onMenuItemSelected(item.id)
+                }
+            }
+        }
+        onMenuActionFinally()
+    }
+
     // ── Effect handling ───────────────────────────────────────────────
     // ── Effect handling ───────────────────────────────────────────────
 
 
     /**
     /**
@@ -1246,3 +1364,47 @@ class ReadBookController(
         }
         }
     }
     }
 }
 }
+
+data class TextMenuState(
+    val selectedText: String,
+    val startX: Int,
+    val startTopY: Int,
+    val startBottomY: Int,
+    val endX: Int,
+    val endBottomY: Int,
+    val items: List<ActionMenuItem>
+)
+
+data class ActionMenuItem(
+    val id: Int,
+    val title: String,
+    val iconDrawable: android.graphics.drawable.Drawable? = null,
+    val intent: Intent? = null,
+    val enabled: Boolean = true
+) {
+    val uniqueId: String
+        get() = if (intent != null) {
+            val comp = intent.component
+            if (comp != null) "${comp.packageName}/${comp.className}" else title
+        } else {
+            when (id) {
+                R.id.menu_copy -> "menu_copy"
+                R.id.menu_share_str -> "menu_share_str"
+                R.id.menu_browser -> "menu_browser"
+                R.id.menu_aloud -> "menu_aloud"
+                R.id.menu_bookmark -> "menu_bookmark"
+                R.id.menu_dict -> "menu_dict"
+                R.id.menu_replace -> "menu_replace"
+                R.id.menu_edit -> "menu_edit"
+                R.id.menu_ai_clean -> "menu_ai_clean"
+                R.id.menu_ai_rewrite -> "menu_ai_rewrite"
+                R.id.menu_search_content -> "menu_search_content"
+                else -> id.toString()
+            }
+        }
+}
+
+data class SelectionMenuConfigItem(
+    val id: String,
+    val enabled: Boolean
+)

+ 29 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -49,6 +49,7 @@ import io.legado.app.ui.browser.WebViewActivity
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.replace.ReplaceEditRoute
 import io.legado.app.ui.replace.ReplaceEditRoute
 import io.legado.app.ui.replace.ReplaceRuleActivity
 import io.legado.app.ui.replace.ReplaceRuleActivity
+import io.legado.app.ui.book.read.sheet.TextSelectMenuConfigSheet
 import io.legado.app.utils.StartActivityContract
 import io.legado.app.utils.StartActivityContract
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
@@ -113,6 +114,7 @@ fun ReadBookRouteScreen(
 ) {
 ) {
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
     val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
+    val textMenuState by controller.textMenuState.collectAsStateWithLifecycle()
     val context = LocalContext.current
     val context = LocalContext.current
     val lifecycleOwner = LocalLifecycleOwner.current
     val lifecycleOwner = LocalLifecycleOwner.current
     val effectsReady = remember(viewModel) { CompletableDeferred<Unit>() }
     val effectsReady = remember(viewModel) { CompletableDeferred<Unit>() }
@@ -453,6 +455,8 @@ fun ReadBookRouteScreen(
 
 
     // ── View layer + Compose UI ───────────────────────────────────────
     // ── View layer + Compose UI ───────────────────────────────────────
 
 
+    var showSelectMenuConfigSheet by remember { mutableStateOf(false) }
+
     Box(Modifier.fillMaxSize()) {
     Box(Modifier.fillMaxSize()) {
         key(controller) {
         key(controller) {
             ReadBookViewLayer(
             ReadBookViewLayer(
@@ -482,6 +486,31 @@ fun ReadBookRouteScreen(
                 onIntent = viewModel::onIntent,
                 onIntent = viewModel::onIntent,
                 onBack = { controller.closeReadBook() },
                 onBack = { controller.closeReadBook() },
             )
             )
+            textMenuState?.let { menuState ->
+                TextActionSelectionMenu(
+                    menuState = menuState,
+                    onDismiss = { controller.dismissTextActionMenu() },
+                    onItemClick = { item -> controller.onTextMenuItemClick(item) },
+                    onOpenManage = {
+                        controller.dismissTextActionMenu()
+                        controller.refs?.readView?.cancelSelect()
+                        showSelectMenuConfigSheet = true
+                    }
+                )
+            }
+            val configItems = remember(showSelectMenuConfigSheet) {
+                if (showSelectMenuConfigSheet) {
+                    controller.getActionMenuItems()
+                } else {
+                    emptyList()
+                }
+            }
+            TextSelectMenuConfigSheet(
+                show = showSelectMenuConfigSheet,
+                items = configItems,
+                onDismissRequest = { showSelectMenuConfigSheet = false },
+                onSaved = { items -> controller.saveMenuConfig(items) }
+            )
         }
         }
     }
     }
 }
 }

+ 0 - 305
app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt

@@ -1,305 +0,0 @@
-package io.legado.app.ui.book.read
-
-import android.annotation.SuppressLint
-import android.app.SearchManager
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ResolveInfo
-import android.view.Gravity
-import android.view.LayoutInflater
-import android.view.Menu
-import android.view.View
-import android.view.ViewGroup
-import android.widget.PopupWindow
-import androidx.appcompat.view.SupportMenuInflater
-import androidx.appcompat.view.menu.MenuBuilder
-import androidx.appcompat.view.menu.MenuItemImpl
-import androidx.core.net.toUri
-import androidx.core.view.isVisible
-import io.legado.app.R
-import io.legado.app.base.adapter.ItemViewHolder
-import io.legado.app.base.adapter.RecyclerAdapter
-import io.legado.app.constant.AppLog
-import io.legado.app.databinding.ItemTextBinding
-import io.legado.app.databinding.PopupActionMenuBinding
-import io.legado.app.ui.config.readConfig.ReadConfig
-import io.legado.app.utils.dpToPx
-import io.legado.app.utils.gone
-import io.legado.app.utils.isAbsUrl
-import io.legado.app.utils.printOnDebug
-import io.legado.app.utils.sendToClip
-import io.legado.app.utils.share
-import io.legado.app.utils.toastOnUi
-import io.legado.app.utils.visible
-
-@SuppressLint("RestrictedApi")
-class TextActionMenu(
-    private val context: Context,
-    private val callBack: CallBack,
-    private val expandTextMenu: () -> Boolean
-) :
-    PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
-
-    private val binding = PopupActionMenuBinding.inflate(LayoutInflater.from(context))
-    private val adapter = Adapter(context).apply {
-        setHasStableIds(true)
-    }
-
-    private var menuItems: List<MenuItemImpl> = emptyList()
-    private val visibleMenuItems = arrayListOf<MenuItemImpl>()
-    private val moreMenuItems = arrayListOf<MenuItemImpl>()
-    private val myMenu = MenuBuilder(context)
-
-    init {
-        @SuppressLint("InflateParams")
-        contentView = binding.root
-
-        isTouchable = true
-        isOutsideTouchable = false
-        isFocusable = false
-        animationStyle = R.style.TextActionMenuAnimation
-
-        SupportMenuInflater(context).inflate(R.menu.content_select_action, myMenu)
-        binding.recyclerView.adapter = adapter
-        binding.recyclerViewMore.adapter = adapter
-        setOnDismissListener {
-            if (!expandTextMenu()) {
-                binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert)
-                binding.recyclerViewMore.gone()
-                adapter.setItems(visibleMenuItems)
-                binding.recyclerView.visible()
-            }
-        }
-        binding.ivMenuMore.setOnClickListener {
-            if (binding.recyclerView.isVisible) {
-                binding.ivMenuMore.setImageResource(R.drawable.ic_arrow_back)
-                adapter.setItems(moreMenuItems)
-                binding.recyclerView.gone()
-                binding.recyclerViewMore.visible()
-            } else {
-                binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert)
-                binding.recyclerViewMore.gone()
-                adapter.setItems(visibleMenuItems)
-                binding.recyclerView.visible()
-            }
-        }
-        upMenu()
-    }
-
-    fun upMenu() {
-        val otherMenu = MenuBuilder(context)
-        onInitializeMenu(otherMenu)
-        menuItems = myMenu.visibleItems + otherMenu.visibleItems
-        visibleMenuItems.clear()
-        moreMenuItems.clear()
-        if (menuItems.size > 6) {
-            visibleMenuItems.addAll(menuItems.subList(0, 6))
-            moreMenuItems.addAll(menuItems.subList(6, menuItems.size))
-        } else {
-            visibleMenuItems.addAll(menuItems)
-        }
-
-        if (expandTextMenu()) {
-            adapter.setItems(menuItems)
-            binding.ivMenuMore.gone()
-        } else {
-            adapter.setItems(visibleMenuItems)
-            binding.ivMenuMore.visible()
-        }
-    }
-
-    fun show(
-        view: View,
-        windowHeight: Int,
-        startX: Int,
-        startTopY: Int,
-        startBottomY: Int,
-        endX: Int,
-        endBottomY: Int
-    ) {
-        if (expandTextMenu()) {
-            when {
-                startTopY > 500 -> {
-                    showAtLocation(
-                        view, Gravity.BOTTOM or Gravity.START, startX, windowHeight - startTopY
-                    )
-                }
-
-                endBottomY - startBottomY > 500 -> {
-                    showAtLocation(view, Gravity.TOP or Gravity.START, startX, startBottomY)
-                }
-
-                else -> {
-                    showAtLocation(view, Gravity.TOP or Gravity.START, endX, endBottomY)
-                }
-            }
-        } else {
-            contentView.measure(
-                View.MeasureSpec.UNSPECIFIED,
-                View.MeasureSpec.UNSPECIFIED,
-            )
-            val popupHeight = contentView.measuredHeight
-            when {
-                startBottomY > 500 -> {
-                    showAtLocation(
-                        view, Gravity.TOP or Gravity.START, startX, startTopY - popupHeight
-                    )
-                }
-
-                endBottomY - startBottomY > 500 -> {
-                    showAtLocation(
-                        view, Gravity.TOP or Gravity.START, startX, startBottomY
-                    )
-                }
-
-                else -> {
-                    showAtLocation(
-                        view, Gravity.TOP or Gravity.START, endX, endBottomY
-                    )
-                }
-            }
-        }
-    }
-
-    inner class Adapter(context: Context) :
-        RecyclerAdapter<MenuItemImpl, ItemTextBinding>(context) {
-
-        override fun getItemId(position: Int): Long {
-            return position.toLong()
-        }
-
-        override fun getViewBinding(parent: ViewGroup): ItemTextBinding {
-            return ItemTextBinding.inflate(inflater, parent, false)
-        }
-
-        override fun convert(
-            holder: ItemViewHolder,
-            binding: ItemTextBinding,
-            item: MenuItemImpl,
-            payloads: MutableList<Any>
-        ) {
-            with(binding) {
-                textView.text = item.title
-                val icon = item.icon
-                if (icon != null && ReadConfig.showSelectMenuIcon) {
-                    val size = 18.dpToPx()
-                    icon.setBounds(0, 0, size, size)
-                    textView.setCompoundDrawables(icon, null, null, null)
-                    textView.compoundDrawablePadding = 6.dpToPx()
-                } else {
-                    textView.setCompoundDrawables(null, null, null, null)
-                }
-            }
-        }
-
-        override fun registerListener(holder: ItemViewHolder, binding: ItemTextBinding) {
-            holder.itemView.setOnClickListener {
-                getItem(holder.layoutPosition)?.let {
-                    if (!callBack.onMenuItemSelected(it.itemId)) {
-                        onMenuItemSelected(it)
-                    }
-                }
-                callBack.onMenuActionFinally()
-            }
-            holder.itemView.setOnLongClickListener {
-                if (ReadConfig.contentSelectSpeakMod == 0) {
-                    ReadConfig.contentSelectSpeakMod = 1
-                    context.toastOnUi("切换为从选择的地方开始一直朗读")
-                } else {
-                    ReadConfig.contentSelectSpeakMod = 0
-                    context.toastOnUi("切换为朗读选择内容")
-                }
-                true
-            }
-        }
-    }
-
-    private fun onMenuItemSelected(item: MenuItemImpl) {
-        when (item.itemId) {
-            R.id.menu_copy -> context.sendToClip(callBack.selectedText)
-            R.id.menu_share_str -> context.share(callBack.selectedText)
-            R.id.menu_browser -> {
-                kotlin.runCatching {
-                    val intent = if (callBack.selectedText.isAbsUrl()) {
-                        Intent(Intent.ACTION_VIEW).apply {
-                            data = callBack.selectedText.toUri()
-                        }
-                    } else {
-                        Intent(Intent.ACTION_WEB_SEARCH).apply {
-                            putExtra(SearchManager.QUERY, callBack.selectedText)
-                        }
-                    }
-                    context.startActivity(intent)
-                }.onFailure {
-                    it.printOnDebug()
-                    context.toastOnUi(it.localizedMessage ?: "ERROR")
-                }
-            }
-
-            else -> item.intent?.let {
-                kotlin.runCatching {
-                    it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText)
-                    context.startActivity(it)
-                }.onFailure { e ->
-                    AppLog.put("执行文本菜单操作出错\n$e", e, true)
-                }
-            }
-        }
-    }
-
-    private fun createProcessTextIntent(): Intent {
-        return Intent()
-            .setAction(Intent.ACTION_PROCESS_TEXT)
-            .setType("text/plain")
-    }
-
-    private fun getSupportedActivities(): List<ResolveInfo> {
-        return context.packageManager
-            .queryIntentActivities(createProcessTextIntent(), 0)
-    }
-
-    private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent {
-        return createProcessTextIntent()
-            .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false)
-            .setClassName(info.activityInfo.packageName, info.activityInfo.name)
-    }
-
-    /**
-     * Start with a menu Item order value that is high enough
-     * so that your "PROCESS_TEXT" menu items appear after the
-     * standard selection menu items like Cut, Copy, Paste.
-     */
-    private fun onInitializeMenu(menu: Menu) {
-        kotlin.runCatching {
-            var menuItemOrder = 100
-            val pm = context.packageManager
-            val filterSet = ReadConfig.textSelectMenuFilter.split(",").filter { it.isNotEmpty() }.toSet()
-            for (resolveInfo in getSupportedActivities()) {
-                val componentName = "${resolveInfo.activityInfo.packageName}/${resolveInfo.activityInfo.name}"
-                if (filterSet.contains(componentName)) {
-                    continue
-                }
-                val menuItem = menu.add(
-                    Menu.NONE, Menu.NONE,
-                    menuItemOrder++, resolveInfo.loadLabel(pm)
-                )
-                menuItem.intent = createProcessTextIntentForResolveInfo(resolveInfo)
-                if (ReadConfig.showSelectMenuIcon) {
-                    menuItem.icon = kotlin.runCatching {
-                        resolveInfo.loadIcon(pm)
-                    }.getOrNull()
-                }
-            }
-        }.onFailure {
-            context.toastOnUi("获取文字操作菜单出错:${it.localizedMessage}")
-        }
-    }
-
-    interface CallBack {
-        val selectedText: String
-
-        fun onMenuItemSelected(itemId: Int): Boolean
-
-        fun onMenuActionFinally()
-    }
-}

+ 312 - 0
app/src/main/java/io/legado/app/ui/book/read/TextActionSelectionMenu.kt

@@ -0,0 +1,312 @@
+package io.legado.app.ui.book.read
+
+import io.legado.app.R
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+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.Settings
+import androidx.compose.material3.VerticalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import coil.compose.AsyncImage
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.NormalCard
+import io.legado.app.ui.widget.components.text.AppText
+
+enum class MenuMode {
+    Quick,
+    More
+}
+
+@Composable
+fun TextActionSelectionMenu(
+    menuState: TextMenuState,
+    onDismiss: () -> Unit,
+    onItemClick: (ActionMenuItem) -> Unit,
+    onOpenManage: () -> Unit
+) {
+    var mode by remember { mutableStateOf(MenuMode.Quick) }
+    val draftItems = menuState.items
+
+    val density = LocalDensity.current.density
+    val positionProvider = remember(menuState, density) {
+        TextMenuPositionProvider(
+            density = density,
+            startX = menuState.startX,
+            startTopY = menuState.startTopY,
+            startBottomY = menuState.startBottomY,
+            endX = menuState.endX,
+            endBottomY = menuState.endBottomY
+        )
+    }
+
+    Popup(
+        popupPositionProvider = positionProvider,
+        onDismissRequest = onDismiss,
+        properties = PopupProperties(
+            focusable = false,
+            dismissOnBackPress = true,
+            dismissOnClickOutside = true
+        )
+    ) {
+        val widthModifier = Modifier.widthIn(max = 300.dp)
+
+        NormalCard(
+            modifier = widthModifier.wrapContentHeight(),
+            containerColor = LegadoTheme.colorScheme.surface,
+            elevation = 6.dp,
+            cornerRadius = 12.dp
+        ) {
+            if (mode == MenuMode.Quick) {
+                QuickMenuView(
+                    items = draftItems,
+                    onItemClick = onItemClick,
+                    onMoreClick = { mode = MenuMode.More }
+                )
+            } else {
+                MoreMenuView(
+                    items = draftItems,
+                    onItemClick = onItemClick,
+                    onBack = { mode = MenuMode.Quick },
+                    onManageClick = {
+                        onDismiss()
+                        onOpenManage()
+                    }
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun QuickMenuView(
+    items: List<ActionMenuItem>,
+    onItemClick: (ActionMenuItem) -> Unit,
+    onMoreClick: () -> Unit
+) {
+    val enabledItems = remember(items) { items.filter { it.enabled } }
+
+    Row(
+        modifier = Modifier
+            .wrapContentWidth()
+            .padding(horizontal = 4.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        LazyRow(
+            modifier = Modifier
+                .weight(1f, fill = false)
+                .padding(vertical = 6.dp),
+            horizontalArrangement = Arrangement.spacedBy(4.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            items(enabledItems, 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)
+        )
+        Box(
+            modifier = Modifier
+                .clickable(onClick = onMoreClick)
+                .padding(horizontal = 12.dp, vertical = 8.dp),
+            contentAlignment = Alignment.Center
+        ) {
+            Icon(
+                imageVector = Icons.Default.MoreHoriz,
+                contentDescription = stringResource(R.string.more_menu),
+                tint = LegadoTheme.colorScheme.onSurface,
+                modifier = Modifier.size(18.dp)
+            )
+        }
+    }
+}
+
+@Composable
+private fun QuickMenuItem(
+    item: ActionMenuItem,
+    onClick: () -> Unit
+) {
+    Row(
+        modifier = Modifier
+            .clickable(onClick = onClick)
+            .padding(horizontal = 12.dp, vertical = 8.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        if (item.iconDrawable != null) {
+            AsyncImage(
+                model = item.iconDrawable,
+                contentDescription = item.title,
+                modifier = Modifier
+                    .size(16.dp)
+                    .padding(end = 4.dp)
+            )
+        }
+        AppText(
+            text = item.title,
+            fontSize = 13.sp,
+            color = LegadoTheme.colorScheme.onSurface,
+            maxLines = 1
+        )
+    }
+}
+
+@Composable
+private fun MoreMenuView(
+    items: List<ActionMenuItem>,
+    onItemClick: (ActionMenuItem) -> Unit,
+    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)
+    ) {
+        Box(
+            modifier = Modifier
+                .fillMaxWidth()
+                .clickable(onClick = onBack)
+                .padding(horizontal = 16.dp, vertical = 8.dp),
+            contentAlignment = Alignment.CenterStart
+        ) {
+            Icon(
+                imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                contentDescription = stringResource(R.string.back),
+                tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                modifier = Modifier.size(18.dp)
+            )
+        }
+        
+        Column(
+            modifier = Modifier
+                .heightIn(max = 240.dp)
+                .verticalScroll(scrollState)
+                .fillMaxWidth()
+        ) {
+            disabledItems.forEach { item ->
+                MoreMenuItem(item = item, onClick = { onItemClick(item) })
+            }
+        }
+
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .clickable(onClick = onManageClick)
+                .padding(horizontal = 16.dp, vertical = 10.dp),
+            verticalAlignment = Alignment.CenterVertically
+        ) {
+            Icon(
+                imageVector = Icons.Default.Settings,
+                contentDescription = stringResource(R.string.edit_menu_items),
+                tint = LegadoTheme.colorScheme.primary,
+                modifier = Modifier
+                    .size(18.dp)
+                    .padding(end = 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
+            )
+        }
+    }
+}
+
+@Composable
+private fun MoreMenuItem(
+    item: ActionMenuItem,
+    onClick: () -> Unit
+) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .clickable(onClick = onClick)
+            .padding(horizontal = 16.dp, vertical = 10.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        if (item.iconDrawable != null) {
+            AsyncImage(
+                model = item.iconDrawable,
+                contentDescription = item.title,
+                modifier = Modifier
+                    .size(18.dp)
+                    .padding(end = 8.dp)
+            )
+        }
+        AppText(
+            text = item.title,
+            fontSize = 13.sp,
+            color = LegadoTheme.colorScheme.onSurface,
+            maxLines = 1
+        )
+    }
+}
+
+
+private class TextMenuPositionProvider(
+    private val density: Float,
+    private val startX: Int,
+    private val startTopY: Int,
+    private val startBottomY: Int,
+    private val endX: Int,
+    private val endBottomY: Int
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        val x: Int
+        val y: Int
+
+        val marginHorizontal = (16 * density).toInt()
+        val marginVertical = (12 * density).toInt()
+        val textMargin = (4 * density).toInt()
+
+        val isSpaceEnoughAtTop = startTopY > popupContentSize.height + textMargin + marginVertical
+        
+        if (isSpaceEnoughAtTop) {
+            x = startX
+            y = startTopY - popupContentSize.height - textMargin
+        } else if (windowSize.height - startBottomY > popupContentSize.height + textMargin + marginVertical) {
+            x = startX
+            y = startBottomY + textMargin
+        } else {
+            x = endX
+            y = endBottomY + textMargin
+        }
+
+        val finalX = x.coerceIn(marginHorizontal, (windowSize.width - popupContentSize.width - marginHorizontal).coerceAtLeast(marginHorizontal))
+        val finalY = y.coerceIn(marginVertical, (windowSize.height - popupContentSize.height - marginVertical).coerceAtLeast(marginVertical))
+
+        return IntOffset(finalX, finalY)
+    }
+}

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

@@ -0,0 +1,176 @@
+package io.legado.app.ui.book.read.sheet
+
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import coil.compose.AsyncImage
+import io.legado.app.R
+import io.legado.app.ui.book.read.ActionMenuItem
+import io.legado.app.ui.theme.LegadoTheme
+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.text.AppText
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun TextSelectMenuConfigSheet(
+    show: Boolean,
+    items: List<ActionMenuItem>,
+    onDismissRequest: () -> Unit,
+    onSaved: (List<ActionMenuItem>) -> Unit
+) {
+    var draftItems by remember(show, items) {
+        mutableStateOf(items)
+    }
+
+    val lazyListState = rememberLazyListState()
+    val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
+        draftItems = draftItems.toMutableList().apply {
+            add(to.index, removeAt(from.index))
+        }
+    }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.edit_select_menu),
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .fillMaxHeight(0.8f)
+                .padding(bottom = 16.dp),
+        ) {
+            LazyColumn(
+                state = lazyListState,
+                verticalArrangement = Arrangement.spacedBy(4.dp),
+                modifier = Modifier
+                    .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()
+                            )
+                        }
+                    }
+                }
+            }
+
+            ConfirmDismissButtonsRow(
+                onDismiss = onDismissRequest,
+                onConfirm = {
+                    onSaved(draftItems)
+                    onDismissRequest()
+                },
+                dismissText = stringResource(R.string.cancel),
+                confirmText = stringResource(R.string.action_save),
+                modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp),
+            )
+        }
+    }
+}
+
+@Composable
+private fun TextSelectMenuConfigItemRow(
+    item: ActionMenuItem,
+    onToggleEnabled: () -> 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) {
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier.size(36.dp),
+            ) {
+                AsyncImage(
+                    model = item.iconDrawable,
+                    contentDescription = item.title,
+                    alpha = alpha,
+                    modifier = Modifier.size(24.dp)
+                )
+            }
+        }
+
+        AppText(
+            text = item.title,
+            fontSize = 14.sp,
+            color = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
+            modifier = Modifier
+                .weight(1f)
+                .padding(horizontal = 12.dp),
+            maxLines = 1
+        )
+
+        IconButton(
+            onClick = onToggleEnabled,
+            modifier = Modifier.size(36.dp),
+        ) {
+            Icon(
+                imageVector = if (item.enabled) {
+                    Icons.Default.Visibility
+                } else {
+                    Icons.Default.VisibilityOff
+                },
+                contentDescription = null,
+                tint = if (item.enabled) {
+                    LegadoTheme.colorScheme.onSurface
+                } else {
+                    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),
+            )
+        }
+    }
+}

+ 3 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt

@@ -90,6 +90,9 @@ object ReadConfig {
     var textSelectMenuFilter
     var textSelectMenuFilter
         get() = ReadMenuConfig.textSelectMenuFilter
         get() = ReadMenuConfig.textSelectMenuFilter
         set(value) { ReadMenuConfig.textSelectMenuFilter = value }
         set(value) { ReadMenuConfig.textSelectMenuFilter = value }
+    var textSelectMenuConfig
+        get() = ReadMenuConfig.textSelectMenuConfig
+        set(value) { ReadMenuConfig.textSelectMenuConfig = value }
     var showReadTitleAddition
     var showReadTitleAddition
         get() = ReadMenuConfig.showReadTitleAddition
         get() = ReadMenuConfig.showReadTitleAddition
         set(value) { ReadMenuConfig.showReadTitleAddition = value }
         set(value) { ReadMenuConfig.showReadTitleAddition = value }

+ 5 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadMenuConfig.kt

@@ -50,6 +50,11 @@ object ReadMenuConfig {
         ""
         ""
     )
     )
 
 
+    var textSelectMenuConfig by prefDelegate(
+        PreferKey.textSelectMenuConfig,
+        ""
+    )
+
     var showReadTitleAddition by prefDelegate(
     var showReadTitleAddition by prefDelegate(
         PreferKey.showReadTitleAddition,
         PreferKey.showReadTitleAddition,
         true
         true

+ 45 - 0
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -2,18 +2,63 @@ package io.legado.app.ui.theme
 
 
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalInspectionMode
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.config.themeConfig.ThemeConfig
+import top.yukonga.miuix.kmp.theme.ColorSchemeMode
+import com.materialkolor.PaletteStyle
 
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 @Composable
 fun AppTheme(
 fun AppTheme(
     darkTheme: Boolean = isSystemInDarkTheme(),
     darkTheme: Boolean = isSystemInDarkTheme(),
     content: @Composable () -> Unit
     content: @Composable () -> Unit
+) {
+    if (LocalInspectionMode.current) {
+        AppThemePreview(darkTheme, content)
+    } else {
+        AppThemeActual(darkTheme, content)
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun AppThemePreview(
+    darkTheme: Boolean,
+    content: @Composable () -> Unit
+) {
+    val colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme()
+    val themeColors = LegadoThemeMode(
+        colorScheme = colorScheme,
+        isDark = darkTheme,
+        seedColor = Color.Unspecified,
+        paletteStyle = PaletteStyle.TonalSpot,
+        themeMode = if (darkTheme) ColorSchemeMode.Dark else ColorSchemeMode.Light,
+        useDynamicColor = false,
+        composeEngine = "material"
+    )
+    CompositionLocalProvider(
+        LocalLegadoThemeColors provides themeColors
+    ) {
+        MaterialThemeWrapper(
+            themeColors = themeColors,
+            customFontFamily = null,
+            content = content
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun AppThemeActual(
+    darkTheme: Boolean,
+    content: @Composable () -> Unit
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
     
     

+ 0 - 55
app/src/main/res/layout/popup_action_menu.xml

@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="wrap_content"
-    android:layout_height="wrap_content"
-    android:orientation="vertical"
-    app:cardBackgroundColor="?attr/colorSurfaceContainer"
-    app:strokeWidth="0dp">
-
-    <LinearLayout
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:animateLayoutChanges="true"
-        android:padding="8dp"
-        android:orientation="vertical">
-        <LinearLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:gravity="center_vertical"
-            android:orientation="horizontal">
-
-            <androidx.recyclerview.widget.RecyclerView
-                android:id="@+id/recycler_view"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:orientation="horizontal"
-                app:flexDirection="row"
-                app:flexWrap="wrap"
-                app:layoutManager="com.google.android.flexbox.FlexboxLayoutManager" />
-
-            <androidx.appcompat.widget.AppCompatImageView
-                android:id="@+id/iv_menu_more"
-                android:layout_width="24dp"
-                android:layout_height="24dp"
-                android:background="?attr/selectableItemBackgroundBorderless"
-                android:contentDescription="@string/more_menu"
-                android:src="@drawable/ic_more_vert"
-                android:visibility="gone" />
-
-        </LinearLayout>
-
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/recycler_view_more"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:orientation="vertical"
-            android:visibility="gone"
-            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
-    </LinearLayout>
-
-
-</com.google.android.material.card.MaterialCardView>
-

+ 0 - 51
app/src/main/res/menu/content_select_action.xml

@@ -1,51 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/menu_replace"
-        android:title="@string/replace" />
-
-    <item
-        android:id="@+id/menu_ai_clean"
-        android:title="@string/ai_text_clean" />
-
-    <item
-        android:id="@+id/menu_ai_rewrite"
-        android:title="@string/ai_text_rewrite" />
-
-    <item
-        android:id="@+id/menu_copy"
-        android:title="@android:string/copy" />
-
-    <item
-        android:id="@+id/menu_bookmark"
-        android:title="@string/bookmark" />
-
-    <item
-        android:id="@+id/menu_aloud"
-        android:title="@string/read_aloud" />
-
-    <item
-        android:id="@+id/menu_dict"
-        android:title="@string/dict" />
-
-    <item
-        android:id="@+id/menu_edit"
-        android:title="@string/edit" />
-
-    <item
-        android:id="@+id/menu_search_content"
-        android:title="@string/search_content"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_browser"
-        android:title="@string/browser"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/menu_share_str"
-        android:title="@string/share"
-        app:showAsAction="never" />
-</menu>

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

@@ -2455,4 +2455,6 @@
     <string name="delete_chapter_cache_message">删除章节缓存:%1$s?</string>
     <string name="delete_chapter_cache_message">删除章节缓存:%1$s?</string>
     <string name="about_open_project_homepage">打开项目主页</string>
     <string name="about_open_project_homepage">打开项目主页</string>
     <string name="about_open_github">打开 GitHub</string>
     <string name="about_open_github">打开 GitHub</string>
+    <string name="edit_menu_items">编辑菜单项</string>
+    <string name="edit_select_menu">编辑选择菜单</string>
 </resources>
 </resources>

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

@@ -2367,4 +2367,6 @@
     <string name="delete_chapter_cache_message">刪除章節緩存:%1$s?</string>
     <string name="delete_chapter_cache_message">刪除章節緩存:%1$s?</string>
     <string name="about_open_project_homepage">開啟項目首頁</string>
     <string name="about_open_project_homepage">開啟項目首頁</string>
     <string name="about_open_github">開啟 GitHub</string>
     <string name="about_open_github">開啟 GitHub</string>
+    <string name="edit_menu_items">編輯選單項</string>
+    <string name="edit_select_menu">編輯選擇選單</string>
 </resources>
 </resources>

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

@@ -2372,4 +2372,6 @@
     <string name="delete_chapter_cache_message">刪除章節快取:%1$s?</string>
     <string name="delete_chapter_cache_message">刪除章節快取:%1$s?</string>
     <string name="about_open_project_homepage">開啟專案首頁</string>
     <string name="about_open_project_homepage">開啟專案首頁</string>
     <string name="about_open_github">開啟 GitHub</string>
     <string name="about_open_github">開啟 GitHub</string>
+    <string name="edit_menu_items">編輯選單項</string>
+    <string name="edit_select_menu">編輯選擇選單</string>
 </resources>
 </resources>

+ 11 - 0
app/src/main/res/values/ids.xml

@@ -19,4 +19,15 @@
     <item name="cursor_right" type="id" />
     <item name="cursor_right" type="id" />
     <item name="navigation_bar" type="id" />
     <item name="navigation_bar" type="id" />
 
 
+    <item name="menu_replace" type="id" />
+    <item name="menu_ai_clean" type="id" />
+    <item name="menu_ai_rewrite" type="id" />
+    <item name="menu_copy" type="id" />
+    <item name="menu_bookmark" type="id" />
+    <item name="menu_aloud" type="id" />
+    <item name="menu_dict" type="id" />
+    <item name="menu_edit" type="id" />
+    <item name="menu_search_content" type="id" />
+    <item name="menu_browser" type="id" />
+    <item name="menu_share_str" type="id" />
 </resources>
 </resources>

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

@@ -2460,4 +2460,6 @@
     <string name="delete_chapter_cache_message">Delete chapter cache: %1$s?</string>
     <string name="delete_chapter_cache_message">Delete chapter cache: %1$s?</string>
     <string name="about_open_project_homepage">Open project homepage</string>
     <string name="about_open_project_homepage">Open project homepage</string>
     <string name="about_open_github">Open GitHub</string>
     <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>
 </resources>
 </resources>