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

fix: 补全修复阅读界面缺失的一些功能和同步问题

HapeLee 5 месяцев назад
Родитель
Сommit
35875f7dd3
62 измененных файлов с 1496 добавлено и 680 удалено
  1. 2 1
      .claude/settings.local.json
  2. 0 5
      app/src/main/AndroidManifest.xml
  3. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 3 1
      app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt
  5. 23 5
      app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt
  6. 10 1
      app/src/main/java/io/legado/app/di/appModule.kt
  7. 3 4
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  8. 3 3
      app/src/main/java/io/legado/app/help/GlideImageGetter.kt
  9. 3 3
      app/src/main/java/io/legado/app/help/WebDavManager.kt
  10. 6 6
      app/src/main/java/io/legado/app/help/book/BookHelp.kt
  11. 2 0
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  12. 6 1
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  13. 4 3
      app/src/main/java/io/legado/app/help/config/ReadStyleResolver.kt
  14. 3 3
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  15. 9 9
      app/src/main/java/io/legado/app/model/ReadBook.kt
  16. 8 9
      app/src/main/java/io/legado/app/model/ReadManga.kt
  17. 6 7
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  18. 5 6
      app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt
  19. 4 5
      app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt
  20. 8 8
      app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt
  21. 24 4
      app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt
  22. 93 16
      app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt
  23. 459 18
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt
  24. 27 29
      app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt
  25. 5 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt
  26. 13 163
      app/src/main/java/io/legado/app/ui/book/read/ReadBookSearchBar.kt
  27. 141 40
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  28. 6 6
      app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt
  29. 3 3
      app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt
  30. 4 4
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  31. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  32. 15 15
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  33. 5 6
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  34. 2 2
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  35. 7 7
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  36. 3 3
      app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt
  37. 111 17
      app/src/main/java/io/legado/app/ui/book/read/sheet/EffectiveReplacesSheet.kt
  38. 0 9
      app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt
  39. 2 54
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt
  40. 25 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt
  41. 0 46
      app/src/main/java/io/legado/app/ui/book/read/sheet/TextTitleSheet.kt
  42. 0 17
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt
  43. 31 0
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt
  44. 2 12
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt
  45. 15 15
      app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt
  46. 212 1
      app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt
  47. 5 2
      app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigContract.kt
  48. 18 4
      app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigScreen.kt
  49. 6 0
      app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigViewModel.kt
  50. 23 1
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
  51. 8 0
      app/src/main/java/io/legado/app/ui/main/MainNavKey.kt
  52. 4 0
      app/src/main/java/io/legado/app/ui/main/MainNavigator.kt
  53. 3 3
      app/src/main/java/io/legado/app/ui/widget/BatteryViewOrgin.kt
  54. 68 0
      app/src/main/java/io/legado/app/ui/widget/components/AppVerticalSlider.kt
  55. 3 3
      app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt
  56. 2 2
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt
  57. 0 93
      app/src/main/res/layout/activity_search_content.xml
  58. 7 0
      app/src/main/res/values-zh-rCN/strings.xml
  59. 11 0
      app/src/main/res/values/array_values.xml
  60. 11 0
      app/src/main/res/values/arrays.xml
  61. 7 0
      app/src/main/res/values/strings.xml
  62. 4 2
      app/src/main/res/xml/pref_config_read.xml

+ 2 - 1
.claude/settings.local.json

@@ -113,7 +113,8 @@
       "Bash(dir /s /b \"%USERPROFILE%\\\\.gradle\\\\caches\\\\*kyant*\")",
       "Bash(Select-String -Pattern \"\\(BUILD|error:|FAILED\\)\")",
       "PowerShell(grep *)",
-      "Bash(powershell -Command \"./gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 30\")"
+      "Bash(powershell -Command \"./gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 30\")",
+      "Bash(git revert *)"
     ]
   }
 }

+ 0 - 5
app/src/main/AndroidManifest.xml

@@ -234,11 +234,6 @@
             android:name=".ui.book.toc.TocActivity"
             android:enableOnBackInvokedCallback="true"
             android:launchMode="singleTop" />
-        <!-- 正文搜索 -->
-        <activity
-            android:name=".ui.book.searchContent.SearchContentActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:launchMode="singleTop" />
         <!-- 订阅源调试 -->
         <activity
             android:name=".ui.rss.source.debug.RssSourceDebugActivity"

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

@@ -163,6 +163,7 @@ object PreferKey {
     const val clearWebViewData = "clearWebViewData"
     const val onlyLatestBackup = "onlyLatestBackup"
     const val brightnessVwPos = "brightnessVwPos"
+    const val brightnessAuto = "brightnessAuto"
     const val shrinkDatabase = "shrinkDatabase"
     const val batchChangeSourceDelay = "batchChangeSourceDelay"
     const val openBookInfoByClickTitle = "openBookInfoByClickTitle"

+ 3 - 1
app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt

@@ -80,7 +80,9 @@ class ReadAloudSettingsRepository(
     }
 
     suspend fun setTtsTimer(value: Int) {
-        ReadConfig.ttsTimer = value.coerceIn(0, 180)
+        val timer = value.coerceIn(0, 180)
+        ReadConfig.ttsTimer = timer
+        settingsRepository.putInt(PreferKey.ttsTimer, timer)
     }
 
     suspend fun setTtsFollowSys(value: Boolean) {

+ 23 - 5
app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt

@@ -29,7 +29,10 @@ data class ReadPreferences(
     val textBottomJustify: Boolean = true,
     val adaptSpecialStyle: Boolean = true,
     val useZhLayout: Boolean = false,
-    val showBrightnessView: Boolean = true,
+    val showBrightnessView: String = "1",
+    val brightnessVwPos: String = "1",
+    val readBrightness: Int = 100,
+    val brightnessAuto: Boolean = false,
     val useUnderline: Boolean = false,
     val readSliderMode: String = "0",
     val doubleHorizontalPage: String = "0",
@@ -157,8 +160,17 @@ class ReadSettingsRepository(
     suspend fun setUseZhLayout(value: Boolean) =
         settingsRepository.putBoolean(PreferKey.useZhLayout, value)
 
-    suspend fun setShowBrightnessView(value: Boolean) =
-        settingsRepository.putBoolean(PreferKey.showBrightnessView, value)
+    suspend fun setShowBrightnessView(value: String) =
+        settingsRepository.putString(PreferKey.showBrightnessView, value)
+
+    suspend fun setBrightnessVwPos(value: String) =
+        settingsRepository.putString(PreferKey.brightnessVwPos, value)
+
+    suspend fun setReadBrightness(value: Int) =
+        settingsRepository.putInt(PreferKey.brightness, value)
+
+    suspend fun setBrightnessAuto(value: Boolean) =
+        settingsRepository.putBoolean(PreferKey.brightnessAuto, value)
 
     suspend fun setUseUnderline(value: Boolean) =
         settingsRepository.putBoolean(PreferKey.useUnderline, value)
@@ -370,7 +382,10 @@ class ReadSettingsRepository(
             textBottomJustify = this[Keys.TextBottomJustify] ?: true,
             adaptSpecialStyle = this[Keys.AdaptSpecialStyle] ?: true,
             useZhLayout = this[Keys.UseZhLayout] ?: false,
-            showBrightnessView = this[Keys.ShowBrightnessView] ?: true,
+            showBrightnessView = this[Keys.ShowBrightnessView] ?: "1",
+            brightnessVwPos = this[Keys.BrightnessVwPos] ?: "1",
+            readBrightness = this[Keys.ReadBrightness] ?: 100,
+            brightnessAuto = this[Keys.BrightnessAuto] ?: false,
             useUnderline = this[Keys.UseUnderline] ?: false,
             readSliderMode = this[Keys.ReadSliderMode] ?: "0",
             doubleHorizontalPage = this[Keys.DoubleHorizontalPage] ?: "0",
@@ -461,7 +476,10 @@ class ReadSettingsRepository(
         val TextBottomJustify = booleanPreferencesKey(PreferKey.textBottomJustify)
         val AdaptSpecialStyle = booleanPreferencesKey(PreferKey.adaptSpecialStyle)
         val UseZhLayout = booleanPreferencesKey(PreferKey.useZhLayout)
-        val ShowBrightnessView = booleanPreferencesKey(PreferKey.showBrightnessView)
+        val ShowBrightnessView = stringPreferencesKey(PreferKey.showBrightnessView)
+        val BrightnessVwPos = stringPreferencesKey(PreferKey.brightnessVwPos)
+        val ReadBrightness = intPreferencesKey(PreferKey.brightness)
+        val BrightnessAuto = booleanPreferencesKey(PreferKey.brightnessAuto)
         val UseUnderline = booleanPreferencesKey(PreferKey.useUnderline)
         val ReadSliderMode = stringPreferencesKey(PreferKey.readSliderMode)
         val DoubleHorizontalPage = stringPreferencesKey(PreferKey.doublePageHorizontal)

+ 10 - 1
app/src/main/java/io/legado/app/di/appModule.kt

@@ -118,6 +118,7 @@ import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
+import io.legado.app.ui.main.MainRouteSearchContent
 import io.legado.app.ui.main.MainViewModel
 import io.legado.app.ui.main.bookshelf.BookshelfViewModel
 import io.legado.app.ui.main.explore.ExploreViewModel
@@ -307,6 +308,14 @@ val appModule = module {
         )
     }
 
-    viewModelOf(::SearchContentViewModel)
+    viewModel { (route: MainRouteSearchContent) ->
+        SearchContentViewModel(
+            bookUrl = route.bookUrl,
+            initialSearchWord = route.searchWord,
+            searchResultIndex = route.searchResultIndex,
+            bookRepository = get(),
+            searchContentRepository = get()
+        )
+    }
 }
 

+ 3 - 4
app/src/main/java/io/legado/app/help/AppWebDav.kt

@@ -17,6 +17,7 @@ import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.model.remote.RemoteBookWebDav
+import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.utils.AlphanumComparator
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
@@ -24,7 +25,6 @@ import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.UrlUtil
 import io.legado.app.utils.compress.ZipUtils
 import io.legado.app.utils.fromJsonObject
-import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.utils.isJson
 import io.legado.app.utils.normalizeFileName
 import io.legado.app.utils.toastOnUi
@@ -33,7 +33,6 @@ import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.runBlocking
 import splitties.init.appCtx
 import java.io.File
-import kotlin.coroutines.coroutineContext
 
 /**
  * webDav初始化会访问网络,不要放到主线程
@@ -359,10 +358,10 @@ object AppWebDav {
         appDb.bookDao.all.forEach { book ->
             val progressFileName = getProgressFileName(book.name, book.author)
             val webDavFile = map[progressFileName]
-            webDavFile ?: return
+            webDavFile ?: return@forEach
             if (webDavFile.lastModify <= book.syncTime) {
                 //本地同步时间大于上传时间不用同步
-                return
+                return@forEach
             }
             getBookProgress(book)?.let { bookProgress ->
                 if (bookProgress.durChapterIndex > book.durChapterIndex

+ 3 - 3
app/src/main/java/io/legado/app/help/GlideImageGetter.kt

@@ -4,7 +4,6 @@ import android.content.Context
 import android.graphics.ColorFilter
 import android.graphics.PixelFormat
 import android.graphics.drawable.Drawable
-import android.os.Build
 import android.text.Html
 import android.widget.TextView
 import com.bumptech.glide.Glide
@@ -95,10 +94,11 @@ class GlideImageGetter(private val context: Context, textView: TextView, private
     }
 
     fun clear() {
-        targets.forEach {
+        val snapshot = targets.toList()
+        targets.clear()
+        snapshot.forEach {
             Glide.with(context).clear(it)
         }
-        targets.clear()
         textViewRef.clear()
     }
 

+ 3 - 3
app/src/main/java/io/legado/app/help/WebDavManager.kt

@@ -3,7 +3,6 @@ package io.legado.app.help
 import android.content.SharedPreferences
 import android.net.Uri
 import io.legado.app.constant.PreferKey
-import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookProgress
@@ -18,6 +17,7 @@ import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavException
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.model.remote.RemoteBookWebDav
+import io.legado.app.ui.config.backupConfig.BackupConfig
 import io.legado.app.utils.AlphanumComparator
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.GSON
@@ -253,9 +253,9 @@ class WebDavManager(
         }
         appDb.bookDao.all.forEach { book ->
             val progressFileName = getProgressFileName(book.name, book.author)
-            val webDavFile = map[progressFileName] ?: return
+            val webDavFile = map[progressFileName] ?: return@forEach
             if (webDavFile.lastModify <= book.syncTime) {
-                return
+                return@forEach
             }
             getBookProgress(book)?.let { bookProgress ->
                 if (bookProgress.durChapterIndex > book.durChapterIndex

+ 6 - 6
app/src/main/java/io/legado/app/help/book/BookHelp.kt

@@ -12,11 +12,11 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
-import io.legado.app.help.config.AppConfig
-import io.legado.app.ui.config.otherConfig.OtherConfig
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.TextFile
+import io.legado.app.ui.config.otherConfig.OtherConfig
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.ImageUtils
@@ -133,12 +133,12 @@ object BookHelp {
     private fun clearComicCache(book: Book) {
         //只处理漫画
         //为0的时候,不清除已缓存数据
-        if (!book.isImage || AppConfig.imageRetainNum == 0) {
+        if (!book.isImage || ReadConfig.imageRetainNum == 0) {
             return
         }
         //向前保留设定数量,向后保留预下载数量
-        val startIndex = book.durChapterIndex - AppConfig.imageRetainNum
-        val endIndex = book.durChapterIndex + AppConfig.preDownloadNum
+        val startIndex = book.durChapterIndex - ReadConfig.imageRetainNum
+        val endIndex = book.durChapterIndex + ReadConfig.preDownloadNum
         val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl, startIndex, endIndex)
         val imgNames = hashSetOf<String>()
         //获取需要保留章节的图片信息
@@ -202,7 +202,7 @@ object BookHelp {
             book.getFolderName(),
             bookChapter.getFileName(),
         ).writeText(content)
-        if (book.isOnLineTxt && AppConfig.tocCountWords) {
+        if (book.isOnLineTxt && ReadConfig.tocCountWords) {
             val wordCount = StringUtils.wordCountFormat(content.length)
             bookChapter.wordCount = wordCount
             appDb.bookChapterDao.update(bookChapter)

+ 2 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -132,6 +132,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
         ReadConfig.defaultSourceChangeAll = preferences.defaultSourceChangeAll
         ReadConfig.sliderVibrator = preferences.sliderVibrator
         ReadConfig.selectVibrator = preferences.selectVibrator
+        ReadConfig.brightnessVwPos = preferences.brightnessVwPos
+        ReadConfig.readBrightness = preferences.readBrightness
     }
 
     fun updateReadBarStyleCache(value: Int) {

+ 6 - 1
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -19,6 +19,7 @@ import io.legado.app.help.DefaultData
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.ui.config.PrefDelegate
 import io.legado.app.ui.config.prefDelegate
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getMeanColor
@@ -208,6 +209,10 @@ object ReadBookConfig {
     var readMenuBorderColorNight by prefDelegate(PreferKey.readMenuBorderColorNight, 0)
     var showTitleBarIcons by prefDelegate(PreferKey.showTitleBarIcons, true)
     var readSliderMode by prefDelegate(PreferKey.readSliderMode, "0")
+    var showBrightnessView by prefDelegate(PreferKey.showBrightnessView, "1")
+    var brightnessVwPos by prefDelegate(PreferKey.brightnessVwPos, "1")
+    var readBrightness by prefDelegate(PreferKey.brightness, 100)
+    var brightnessAuto by prefDelegate(PreferKey.brightnessAuto, false)
 
     var styleSelect: Int
         get() = if (isComic) comicStyleSelect else readStyleSelect
@@ -301,7 +306,7 @@ object ReadBookConfig {
     val resolvedMenuBgColor: Int
         get() {
             val isNight = ReadStyleResolver.isNightTheme()
-            return when (AppConfig.readBarStyle) {
+            return when (ReadConfig.readBarStyle) {
                 1 -> { // 跟随阅读背景
                     val background = ReadStyleResolver.currentBackground(durConfig)
                     if (background.type == 0) {

+ 4 - 3
app/src/main/java/io/legado/app/help/config/ReadStyleResolver.kt

@@ -5,6 +5,7 @@ import android.graphics.drawable.Drawable
 import androidx.core.graphics.drawable.toDrawable
 import androidx.core.graphics.toColorInt
 import com.google.android.material.color.MaterialColors
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.externalFiles
@@ -28,14 +29,14 @@ object ReadStyleResolver {
 
     fun currentMode(): ReadStyleMode {
         return when {
-            AppConfig.isEInkMode -> ReadStyleMode.EInk
-            AppConfig.isNightTheme -> ReadStyleMode.Night
+            ReadConfig.isEInkMode -> ReadStyleMode.EInk
+            ReadConfig.isNightTheme -> ReadStyleMode.Night
             else -> ReadStyleMode.Day
         }
     }
 
     fun isNightTheme(): Boolean {
-        return AppConfig.isNightTheme
+        return ReadConfig.isNightTheme
     }
 
     fun setCurrentBackground(

+ 3 - 3
app/src/main/java/io/legado/app/model/ReadAloud.kt

@@ -8,10 +8,10 @@ import io.legado.app.constant.EventBus
 import io.legado.app.constant.IntentAction
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.HttpTTS
-import io.legado.app.help.config.AppConfig
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.service.HttpReadAloudService
 import io.legado.app.service.TTSReadAloudService
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.StringUtils
 import io.legado.app.utils.postEvent
@@ -21,7 +21,7 @@ import splitties.init.appCtx
 
 object ReadAloud {
     private var aloudClass: Class<*> = getReadAloudClass()
-    val ttsEngine get() = ReadBook.book?.getTtsEngine() ?: AppConfig.ttsEngine
+    val ttsEngine get() = ReadBook.book?.getTtsEngine() ?: ReadConfig.ttsEngine
     var httpTTS: HttpTTS? = null
 
     private fun getReadAloudClass(): Class<*> {
@@ -134,4 +134,4 @@ object ReadAloud {
         }
     }
 
-}
+}

+ 9 - 9
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -21,7 +21,6 @@ import io.legado.app.help.book.isSameNameAuthor
 import io.legado.app.help.book.readSimulating
 import io.legado.app.help.book.simulatedTotalChapterNum
 import io.legado.app.help.book.update
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.globalExecutor
@@ -29,7 +28,6 @@ import io.legado.app.model.localBook.TextFile
 import io.legado.app.model.translation.TranslationChapterState
 import io.legado.app.model.translation.TranslationChapterStatus
 import io.legado.app.model.translation.TranslationManager
-import kotlinx.coroutines.flow.MutableStateFlow
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.service.CacheBookService
@@ -37,6 +35,7 @@ import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
@@ -51,6 +50,7 @@ import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
@@ -211,7 +211,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         ReadBookConfig.isComic = book.isImage
         if (oldIndex != ReadBookConfig.styleSelect) {
             postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
-            if (AppConfig.readBarStyleFollowPage) {
+            if (ReadConfig.readBarStyleFollowPage) {
                 postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
             }
         }
@@ -288,7 +288,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         uploadSuccessAction: (() -> Unit)? = null,
         syncSuccessAction: (() -> Unit)? = null
     ) {
-        if (!AppConfig.syncBookProgress) return
+        if (!ReadConfig.syncBookProgress) return
         val book = book ?: return
         Coroutine.async {
             AppWebDav.getBookProgress(book)
@@ -301,7 +301,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
             ) {
                 // 服务器没有进度或者进度比服务器快,上传现有进度
                 Coroutine.async {
-                    AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction)
+                    AppWebDav.uploadBookProgress(book, onSuccess = uploadSuccessAction)
                     book.update()
                 }
             } else if (progress.durChapterIndex > book.durChapterIndex ||
@@ -560,7 +560,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
     }
 
     fun recycleRecorders(beforeIndex: Int, afterIndex: Int) {
-        if (!AppConfig.optimizeRender) {
+        if (!ReadConfig.optimizeRender) {
             return
         }
         executor.execute {
@@ -1131,7 +1131,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
     private fun preDownload() {
         if (book?.isLocal == true) return
         executor.execute {
-            if (AppConfig.preDownloadNum < 2) {
+            if (ReadConfig.preDownloadNum < 2) {
                 return@execute
             }
             preDownloadTask?.cancel()
@@ -1139,7 +1139,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                 //预下载
                 launch {
                     val maxChapterIndex =
-                        min(durChapterIndex + AppConfig.preDownloadNum, chapterSize)
+                        min(durChapterIndex + ReadConfig.preDownloadNum, chapterSize)
                     for (i in durChapterIndex.plus(2)..maxChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue
@@ -1147,7 +1147,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
                     }
                 }
                 launch {
-                    val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum)
+                    val minChapterIndex = durChapterIndex - min(5, ReadConfig.preDownloadNum)
                     for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue

+ 8 - 9
app/src/main/java/io/legado/app/model/ReadManga.kt

@@ -18,8 +18,6 @@ import io.legado.app.help.book.isSameNameAuthor
 import io.legado.app.help.book.readSimulating
 import io.legado.app.help.book.simulatedTotalChapterNum
 import io.legado.app.help.book.update
-import io.legado.app.help.config.AppConfig
-import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.globalExecutor
 import io.legado.app.model.webBook.WebBook
@@ -28,6 +26,8 @@ import io.legado.app.ui.book.manga.entities.MangaChapter
 import io.legado.app.ui.book.manga.entities.MangaContent
 import io.legado.app.ui.book.manga.entities.MangaPage
 import io.legado.app.ui.book.manga.entities.ReaderLoading
+import io.legado.app.ui.config.readConfig.ReadConfig
+import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.utils.mapIndexed
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
@@ -45,7 +45,6 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Semaphore
 import org.koin.core.component.KoinComponent
 import org.koin.core.component.inject
-import kotlin.getValue
 import kotlin.math.min
 
 @Suppress("MemberVisibilityCanBePrivate")
@@ -472,7 +471,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
     private fun preDownload() {
         if (book?.isLocal == true) return
         executor.execute {
-            if (AppConfig.preDownloadNum < 2) {
+            if (ReadConfig.preDownloadNum < 2) {
                 return@execute
             }
             preDownloadTask?.cancel()
@@ -480,7 +479,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
                 //预下载
                 launch {
                     val maxChapterIndex =
-                        min(durChapterIndex + AppConfig.preDownloadNum, chapterSize)
+                        min(durChapterIndex + ReadConfig.preDownloadNum, chapterSize)
                     for (i in durChapterIndex.plus(2)..maxChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue
@@ -488,7 +487,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
                     }
                 }
                 launch {
-                    val minChapterIndex = durChapterIndex - min(5, AppConfig.preDownloadNum)
+                    val minChapterIndex = durChapterIndex - min(5, ReadConfig.preDownloadNum)
                     for (i in durChapterIndex.minus(2) downTo minChapterIndex) {
                         if (downloadedChapters.contains(i)) continue
                         if ((downloadFailChapters[i] ?: 0) >= 3) continue
@@ -594,7 +593,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
         uploadSuccessAction: (() -> Unit)? = null,
         syncSuccessAction: (() -> Unit)? = null,
     ) {
-        if (!AppConfig.syncBookProgress) return
+        if (!ReadConfig.syncBookProgress) return
         val book = book ?: return
         Coroutine.async {
             AppWebDav.getBookProgress(book)
@@ -607,7 +606,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
             ) {
                 // 服务器没有进度或者进度比服务器快,上传现有进度
                 Coroutine.async {
-                    AppWebDav.uploadBookProgress(BookProgress(book), uploadSuccessAction)
+                    AppWebDav.uploadBookProgress(book, onSuccess = uploadSuccessAction)
                     book.update()
                 }
             } else if (progress.durChapterIndex > book.durChapterIndex ||
@@ -721,4 +720,4 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
         fun showLoading()
         fun startLoad()
     }
-}
+}

+ 6 - 7
app/src/main/java/io/legado/app/service/HttpReadAloudService.kt

@@ -34,8 +34,6 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.HttpTTS
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
-import io.legado.app.help.config.AppConfig
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.exoplayer.InputStreamDataSource
 import io.legado.app.help.http.okHttpClient
@@ -43,6 +41,7 @@ import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
 import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.ui.book.read.page.entities.TextChapter
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.printOnDebug
@@ -98,7 +97,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     private val loadErrorHandlingPolicy by lazy {
         CustomLoadErrorHandlingPolicy()
     }
-    private var speechRate: Int = AppConfig.speechRatePlay + 5
+    private var speechRate: Int = ReadConfig.speechRatePlay + 5
     private var downloadTask: Coroutine<*>? = null
     private var playIndexJob: Job? = null
     private var downloadErrorNo: Int = 0
@@ -210,7 +209,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     private suspend fun preDownloadAudios(httpTts: HttpTTS) {
         val book = ReadBook.book ?: return
         val currentIdx = ReadBook.durChapterIndex
-        val limit = AppConfig.audioPreDownloadNum
+        val limit = ReadConfig.audioPreDownloadNum
         
         try {
             for (i in 1..limit) {
@@ -300,7 +299,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     ) {
         val book = ReadBook.book ?: return
         val currentIdx = ReadBook.durChapterIndex
-        val limit = AppConfig.audioPreDownloadNum
+        val limit = ReadConfig.audioPreDownloadNum
         
         try {
             for (i in 1..limit) {
@@ -491,7 +490,7 @@ class HttpReadAloudService : BaseReadAloudService(),
      * 如果时间设置为0,则不再保护当前章节,退出即全删。
      */
     private fun removeCacheFile() {
-        val keepTime = AppConfig.audioCacheCleanTime
+        val keepTime = ReadConfig.audioCacheCleanTime
         // 只有当时间大于0时,才需要保护当前章节。如果为0,说明用户想彻底不留缓存。
         val protectCurrentChapter = keepTime > 0
         val titleMd5 = if (protectCurrentChapter) MD5Utils.md5Encode16(this.textChapter?.chapter?.title ?: "") else ""
@@ -572,7 +571,7 @@ class HttpReadAloudService : BaseReadAloudService(),
     override fun upSpeechRate(reset: Boolean) {
         downloadTask?.cancel()
         exoPlayer.stop()
-        speechRate = AppConfig.speechRatePlay + 5
+        speechRate = ReadConfig.speechRatePlay + 5
         if (ReadConfig.streamReadAloudAudio) {
             downloadAndPlayAudiosStream()
         } else {

+ 5 - 6
app/src/main/java/io/legado/app/ui/book/manga/ReadMangaActivity.kt

@@ -38,12 +38,9 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityMangaBinding
 import io.legado.app.databinding.ViewLoadMoreBinding
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppFreezeMonitor.handler
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isLocal
-import io.legado.app.help.config.AppConfig
-import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.source.getSourceType
 import io.legado.app.help.storage.Backup
@@ -78,6 +75,8 @@ import io.legado.app.ui.book.read.observeEyeProtectionEvents
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.toc.TocActivityResult
 import io.legado.app.ui.browser.WebViewActivity
+import io.legado.app.ui.config.readConfig.ReadConfig
+import io.legado.app.ui.config.readMangaConfig.ReadMangaConfig
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.widget.number.NumberPickerDialog
 import io.legado.app.ui.widget.recycler.LoadMoreView
@@ -220,7 +219,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
             binding.llRetry.isGone = true
             ReadManga.loadOrUpContent()
         }
-        binding.flLoading.isVisible = !AppConfig.isEInkMode
+        binding.flLoading.isVisible = !ReadConfig.isEInkMode
         binding.eyeProtectionOverlay.refresh()
         mAdapter.addFooterView {
             ViewLoadMoreBinding.bind(loadMoreView)
@@ -458,7 +457,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         networkChangedListener.register()
         networkChangedListener.onNetworkChanged = {
             // 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑)
-            if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) {
+            if (ReadConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) {
                 ReadManga.syncProgress({ progress -> sureNewProgress(progress) })
             }
         }
@@ -480,7 +479,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
         if (ReadManga.inBookshelf) {
             ReadManga.saveRead()
             if (!BuildConfig.DEBUG) {
-                if (AppConfig.syncBookProgressPlus) {
+                if (ReadConfig.syncBookProgressPlus) {
                     ReadManga.syncProgress()
                 } else {
                     ReadManga.uploadProgress()

+ 4 - 5
app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt

@@ -23,9 +23,7 @@ import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocalModified
 import io.legado.app.help.book.removeType
 import io.legado.app.help.book.simulatedTotalChapterNum
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.coroutine.Coroutine
-import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadManga
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.webBook.WebBook
@@ -48,6 +46,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import java.io.ByteArrayOutputStream
+import io.legado.app.ui.config.readConfig.ReadConfig as GlobalReadConfig
 
 class ReadMangaViewModel(
     application: Application,
@@ -112,7 +111,7 @@ class ReadMangaViewModel(
             // 有章节跳转不同步阅读进度
             ReadManga.chapterChanged = false
         } else if (!isSameBook) {
-            if (AppConfig.syncBookProgressPlus) {
+            if (GlobalReadConfig.syncBookProgressPlus) {
                 ReadManga.syncProgress(
                     { progress -> ReadManga.mCallback?.sureNewProgress(progress) })
             } else {
@@ -195,7 +194,7 @@ class ReadMangaViewModel(
      * 自动换源
      */
     private fun autoChangeSource(name: String, author: String) {
-        if (!AppConfig.autoChangeSource) return
+        if (!GlobalReadConfig.autoChangeSource) return
         execute {
             val sources = appDb.bookSourceDao.allTextEnabledPart
             flow {
@@ -246,7 +245,7 @@ class ReadMangaViewModel(
         book: Book,
         alertSync: ((progress: BookProgress) -> Unit)? = null
     ) {
-        if (!AppConfig.syncBookProgress) return
+        if (!GlobalReadConfig.syncBookProgress) return
         execute {
             getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
         }.onError {

+ 8 - 8
app/src/main/java/io/legado/app/ui/book/read/MangaMenu.kt

@@ -14,12 +14,12 @@ import androidx.core.view.isVisible
 import com.google.android.material.slider.Slider
 import io.legado.app.R
 import io.legado.app.databinding.ViewMangaMenuBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.source.getSourceType
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadManga
 import io.legado.app.ui.browser.WebViewActivity
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.activity
 import io.legado.app.utils.applyNavigationBarPadding
 import io.legado.app.utils.gone
@@ -112,13 +112,13 @@ class MangaMenu @JvmOverloads constructor(
 //        val brightnessBackground = GradientDrawable()
 //        brightnessBackground.cornerRadius = 5F.dpToPx()
 //        //brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f))
-//        if (AppConfig.isEInkMode) {
+//        if (ReadConfig.isEInkMode) {
 //            titleBar.setBackgroundResource(R.drawable.bg_eink_border_bottom)
 //            bottomMenu.setBackgroundResource(R.drawable.bg_eink_border_top)
 //        } else {
 //            //bottomMenu.setBackgroundColor(bgColor)
 //        }
-        if (AppConfig.showReadTitleBarAddition) {
+        if (ReadConfig.showReadTitleAddition) {
             titleBarAddition.visible()
         } else {
             titleBarAddition.gone()
@@ -134,7 +134,7 @@ class MangaMenu @JvmOverloads constructor(
         menuTopOut.setAnimationListener(menuOutListener)
     }
 
-    fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode) {
+    fun runMenuOut(anim: Boolean = !ReadConfig.isEInkMode) {
         if (isMenuOutAnimating) {
             return
         }
@@ -149,7 +149,7 @@ class MangaMenu @JvmOverloads constructor(
         }
     }
 
-    fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
+    fun runMenuIn(anim: Boolean = !ReadConfig.isEInkMode) {
         this.visible()
         binding.titleBar.visible()
         binding.bottomMenu.visible()
@@ -181,7 +181,7 @@ class MangaMenu @JvmOverloads constructor(
             callBack.openBookInfoActivity()
         }
         val chapterViewClickListener = OnClickListener {
-            if (AppConfig.readUrlInBrowser) {
+            if (ReadConfig.readUrlInBrowser) {
                 context.openUrl(tvChapterUrl.text.toString().substringBefore(",{"))
             } else {
                 context.startActivity<WebViewActivity> {
@@ -199,10 +199,10 @@ class MangaMenu @JvmOverloads constructor(
             context.alert(R.string.open_fun) {
                 setMessage(R.string.use_browser_open)
                 okButton {
-                    AppConfig.readUrlInBrowser = true
+                    ReadConfig.readUrlInBrowser = true
                 }
                 noButton {
-                    AppConfig.readUrlInBrowser = false
+                    ReadConfig.readUrlInBrowser = false
                 }
             }
             true

+ 24 - 4
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -196,6 +196,10 @@ data class ReadMenuConfig(
     val readMenuCustomIcons: ImmutableMap<String, String> = persistentMapOf(),
     val titleBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
     val bottomBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
+    val showBrightnessView: String = "1",
+    val brightnessVwPos: String = "1",
+    val readBrightness: Int = 100,
+    val brightnessAuto: Boolean = false,
 )
 
 @Immutable
@@ -300,7 +304,7 @@ sealed interface ReadBookIntent {
 
     // Brightness
     data class SetBrightness(val value: Int) : ReadBookIntent
-    data object ToggleBrightnessAuto : ReadBookIntent
+    data class ToggleBrightnessAuto(val auto: Boolean) : ReadBookIntent
 
     // Seek bar jump
     data class SeekToChapter(val index: Int) : ReadBookIntent
@@ -526,7 +530,7 @@ sealed interface ReadBookEffect {
     data object CancelSelect : ReadBookEffect
     data object UpSystemUiVisibility : ReadBookEffect
     data class SetBrightness(val value: Int) : ReadBookEffect
-    data object ToggleBrightnessAuto : ReadBookEffect
+    data class ToggleBrightnessAuto(val auto: Boolean, val value: Int) : ReadBookEffect
 
     // Read aloud / auto page
     data object ToggleReadAloud : ReadBookEffect
@@ -535,7 +539,15 @@ sealed interface ReadBookEffect {
 
     // Search
     data class OpenSearchActivity(val word: String?, val bookUrl: String) : ReadBookEffect
-    data class NavigateToSearchResult(val result: SearchResult) : ReadBookEffect
+    data class NavigateToSearchResult(
+        val result: SearchResult,
+        val chapterIndex: Int,
+        val pageIndex: Int,
+        val lineIndex: Int,
+        val startCharIndex: Int,
+        val endLineIndex: Int,
+        val endCharIndex: Int,
+    ) : ReadBookEffect
     data object ExitSearch : ReadBookEffect
 
     // Source actions
@@ -1040,7 +1052,15 @@ sealed interface ConfigUpdate {
     data class UseZhLayout(val value: Boolean) : ConfigUpdate {
         override val actions = setOf(ConfigUpdateAction.ReloadContent)
     }
-    data class ShowBrightnessView(val value: Boolean) : ConfigUpdate {
+    data class ShowBrightnessView(val value: String) : ConfigUpdate {
+        override val actions = emptySet<ConfigUpdateAction>()
+    }
+
+    data class BrightnessVwPos(val value: String) : ConfigUpdate {
+        override val actions = emptySet<ConfigUpdateAction>()
+    }
+
+    data class BrightnessAuto(val value: Boolean) : ConfigUpdate {
         override val actions = emptySet<ConfigUpdateAction>()
     }
     data class UseUnderlineGlobal(val value: Boolean) : ConfigUpdate {

+ 93 - 16
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -9,6 +9,7 @@ import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.View
 import android.view.WindowInsets
+import android.view.WindowManager
 import androidx.appcompat.app.AppCompatActivity
 import androidx.core.content.ContextCompat
 import androidx.core.view.HapticFeedbackConstantsCompat
@@ -23,7 +24,6 @@ import io.legado.app.constant.BookType
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.help.TTS
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.storage.Backup
 import io.legado.app.lib.dialogs.SelectItem
@@ -40,8 +40,10 @@ import io.legado.app.service.BaseReadAloudService
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.login.SourceLoginJsExtensions
 import io.legado.app.ui.widget.PopupAction
 import io.legado.app.utils.ColorUtils
@@ -60,6 +62,7 @@ import io.legado.app.utils.throttle
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.visible
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.launch
 
 /**
@@ -341,7 +344,7 @@ class ReadBookController(
         r.textMenuPosition.x = x
         r.textMenuPosition.y = top
 
-        if (AppConfig.selectVibrator) {
+        if (ReadConfig.selectVibrator) {
             r.root.performHapticFeedback(HapticFeedbackConstantsCompat.TEXT_HANDLE_MOVE)
         }
     }
@@ -351,7 +354,7 @@ class ReadBookController(
         r.cursorRight.x = x
         r.cursorRight.y = y
         r.cursorRight.visible(true)
-        if (AppConfig.selectVibrator) {
+        if (ReadConfig.selectVibrator) {
             r.root.performHapticFeedback(HapticFeedbackConstantsCompat.TEXT_HANDLE_MOVE)
         }
     }
@@ -616,7 +619,7 @@ class ReadBookController(
             is ReadBookEffect.LongToast -> activity.longToastOnUi(effect.message)
             is ReadBookEffect.SetBrightness -> {
                 val lp = activity.window.attributes
-                lp.screenBrightness = effect.value / 255f
+                lp.screenBrightness = effect.value / 100f
                 activity.window.attributes = lp
             }
 
@@ -682,7 +685,14 @@ class ReadBookController(
                 screenOffTimerStart()
             }
 
-            is ReadBookEffect.ToggleBrightnessAuto -> { /* TODO */
+            is ReadBookEffect.ToggleBrightnessAuto -> {
+                val lp = activity.window.attributes
+                if (effect.auto) {
+                    lp.screenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
+                } else {
+                    lp.screenBrightness = effect.value / 100f
+                }
+                activity.window.attributes = lp
             }
 
             // ── Phase 4: Activity-dependent effects ──
@@ -695,15 +705,48 @@ class ReadBookController(
 
             is ReadBookEffect.TextActionSpeak -> speak(effect.text)
             is ReadBookEffect.NavigateToSearchResult -> {
-                // Navigate handled by ReadView — no external callback needed
+                if (effect.pageIndex < 0) {
+                    // Chapter not loaded — open it, then mark in the success callback
+                    ReadBook.openChapter(effect.chapterIndex) {
+                        val tc = ReadBook.curTextChapter ?: return@openChapter
+                        val query = effect.result.query
+                        val pos = viewModel.searchResultPositions(tc, effect.result, query)
+                        val lineIndex = pos[1]
+                        val charIndex = pos[2]
+                        val addLine = pos[3]
+                        val charIndex2 = pos[4]
+                        val queryLength = query.length
+                        val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
+                        val endCharIndex = if (addLine == 0) {
+                            charIndex + queryLength
+                        } else {
+                            charIndex2 + 1
+                        }
+                        activity.lifecycleScope.launch(Main) {
+                            navigatePageByPos(tc, pos[0])
+                            markSearchResultOnPage(
+                                intArrayOf(
+                                    pos[0], lineIndex, charIndex, endLineIndex, endCharIndex
+                                )
+                            )
+                        }
+                    }
+                } else {
+                    // Same chapter — navigate to page, then mark
+                    val tc = ReadBook.curTextChapter ?: return
+                    navigatePageByPos(tc, effect.pageIndex)
+                    markSearchResultOnPage(
+                        intArrayOf(
+                            effect.pageIndex, effect.lineIndex,
+                            effect.startCharIndex, effect.endLineIndex, effect.endCharIndex
+                        )
+                    )
+                }
             }
 
             is ReadBookEffect.ExitSearch -> {
-                if (viewModel.uiState.value.isShowingSearchResult) {
-                    viewModel.onIntent(ReadBookIntent.SetShowingSearchResult(false))
-                    ReadBook.clearSearchResult()
-                    refs?.readView?.cancelSelect(true)
-                }
+                ReadBook.clearSearchResult()
+                refs?.readView?.cancelSelect(clearSearchResult = true)
             }
 
             is ReadBookEffect.SyncBookProgress -> {
@@ -935,17 +978,17 @@ class ReadBookController(
     }
 
     override fun mouseWheelPage(direction: PageDirection) {
-        if (menuLayoutIsVisible || !AppConfig.mouseWheelPage) {
+        if (menuLayoutIsVisible || !ReadConfig.mouseWheelPage) {
             return
         }
         keyPageDebounce(direction, mouseWheel = true, longPress = false)
     }
 
     private fun volumeKeyPage(direction: PageDirection, longPress: Boolean): Boolean {
-        if (!AppConfig.volumeKeyPage) {
+        if (!ReadConfig.volumeKeyPage) {
             return false
         }
-        if (!AppConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) {
+        if (!ReadConfig.volumeKeyPageOnPlay && BaseReadAloudService.isPlay()) {
             return false
         }
         handleKeyPage(direction, longPress)
@@ -953,7 +996,7 @@ class ReadBookController(
     }
 
     override fun handleKeyPage(direction: PageDirection, longPress: Boolean) {
-        if (AppConfig.keyPageOnLongPress || direction == PageDirection.NONE) {
+        if (ReadConfig.keyPageOnLongPress || direction == PageDirection.NONE) {
             keyPage(direction)
         } else {
             keyPageDebounce(direction, longPress = longPress)
@@ -1057,7 +1100,7 @@ class ReadBookController(
     }
 
     fun setOrientation() {
-        when (AppConfig.screenOrientation) {
+        when (ReadConfig.screenOrientation) {
             "0" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
             "1" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
             "2" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
@@ -1066,4 +1109,38 @@ class ReadBookController(
             "5" -> activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
         }
     }
+
+    // ── Search result navigation helpers ─────────────────────────────
+
+    /**
+     * Navigate to the page at [pageIndex] within the current chapter.
+     */
+    private fun navigatePageByPos(textChapter: TextChapter, pageIndex: Int) {
+        val pagePos = textChapter.getReadLength(pageIndex)
+        if (ReadBook.durChapterPos != pagePos) {
+            ReadBook.durChapterPos = pagePos
+            ReadBook.callBack?.upContent()
+            ReadBook.callBack?.cancelSelect()
+        }
+    }
+
+    /**
+     * Mark search result columns on the current page for highlighting.
+     * @param pos array of [pageIndex, lineIndex, startCharIndex, endLineIndex, endCharIndex]
+     */
+    private fun markSearchResultOnPage(pos: IntArray) {
+        val readView = refs?.readView ?: return
+        val lineIndex = pos[1]
+        val startCharIndex = pos[2]
+        val endLineIndex = pos[3]
+        val endCharIndex = pos[4]
+        readView.cancelSelect(clearSearchResult = true)
+        isSelectingSearchResult = true
+        try {
+            readView.curPage.selectStartMoveIndex(0, lineIndex, startCharIndex)
+            readView.curPage.selectEndMoveIndex(0, endLineIndex, endCharIndex)
+        } finally {
+            isSelectingSearchResult = false
+        }
+    }
 }

+ 459 - 18
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -6,11 +6,15 @@ import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.SizeTransform
 import androidx.compose.animation.animateContentSize
+import androidx.compose.animation.core.LinearOutSlowInEasing
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutHorizontally
 import androidx.compose.animation.slideOutVertically
 import androidx.compose.animation.togetherWith
 import androidx.compose.foundation.BorderStroke
@@ -27,6 +31,7 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsetsSides
@@ -54,10 +59,14 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.ArrowForward
 import androidx.compose.material.icons.automirrored.filled.Login
 import androidx.compose.material.icons.filled.Block
+import androidx.compose.material.icons.filled.BrightnessAuto
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Menu
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Payment
 import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Surface
@@ -126,7 +135,6 @@ import io.legado.app.R
 import io.legado.app.constant.ReadMenuBlurMode
 import io.legado.app.constant.ReadMenuBlurStyle
 import io.legado.app.data.entities.Book
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.ui.animation.DampedDragAnimation
 import io.legado.app.ui.animation.InteractiveHighlight
@@ -137,9 +145,11 @@ import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
 import io.legado.app.ui.book.read.sheet.ReadStyleContent
 import io.legado.app.ui.book.read.sheet.ReadStyleTextTitleContent
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.hazeStyle.HazeLegado
 import io.legado.app.ui.widget.components.AppSlider
+import io.legado.app.ui.widget.components.AppVerticalSlider
 import io.legado.app.ui.widget.components.bookmark.BookmarkEditContent
 import io.legado.app.ui.widget.components.button.series.SmallTonalButton
 import io.legado.app.ui.widget.components.divider.PillDivider
@@ -168,12 +178,29 @@ fun ReadBookMenuBar(
 ) {
     val context = LocalContext.current
     val currentRoute = state.menuState.currentRoute
+    val searchMenuVisible = state.isShowingSearchResult &&
+            state.searchMenuVisible &&
+            !state.menuVisible
+    val contentTarget = if (searchMenuVisible) {
+        ReadBookMenuContent.Search
+    } else {
+        ReadBookMenuContent.Route(currentRoute)
+    }
     val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig
+    var readStylePage by remember { mutableIntStateOf(0) }
+    LaunchedEffect(currentRoute) {
+        if (currentRoute != ReadBookMenuRoute.ReadStyle) {
+            readStylePage = 0
+        }
+    }
+    val hideTopBar = dialogLikeRoute ||
+            currentRoute == ReadBookMenuRoute.ReadStyle && readStylePage >= 1 ||
+            currentRoute == ReadBookMenuRoute.TextTitle
     val menuColors = readMenuColors()
 
     Box(Modifier.fillMaxSize()) {
         AnimatedVisibility(
-            visible = state.menuVisible,
+            visible = state.menuVisible || searchMenuVisible,
             enter = fadeIn(),
             exit = fadeOut(),
         ) {
@@ -183,13 +210,19 @@ fun ReadBookMenuBar(
                     .clickable(
                         indication = null,
                         interactionSource = remember { MutableInteractionSource() },
-                    ) { onIntent(ReadBookIntent.HideMenu) }
+                    ) {
+                        if (searchMenuVisible) {
+                            onIntent(ReadBookIntent.HideSearchMenu)
+                        } else {
+                            onIntent(ReadBookIntent.HideMenu)
+                        }
+                    }
             )
         }
 
         // Top title bar + floating icon row (top positions)
         AnimatedVisibility(
-            visible = state.menuVisible && !dialogLikeRoute,
+            visible = state.menuVisible && !hideTopBar,
             enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
             exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut(),
             modifier = Modifier.align(Alignment.TopCenter),
@@ -218,15 +251,97 @@ fun ReadBookMenuBar(
             }
         }
 
+        // Vertical brightness bar (right or left side)
+        val brightnessMode = state.menuConfig.showBrightnessView
+        val brightnessVwPos = state.menuConfig.brightnessVwPos
+        val brightnessIsLeft = brightnessVwPos == "0"
+        val brightnessShape = RoundedCornerShape(40.dp)
+        val useBrightnessHaze = readMenuBottomBarHazeEnabled(
+            hazeState = hazeState,
+            menuConfig = state.menuConfig,
+            isFloating = false,
+        )
+        AnimatedVisibility(
+            visible = brightnessMode == "2" && state.menuVisible,
+            enter = slideInHorizontally(
+                initialOffsetX = { if (brightnessIsLeft) -it else it }
+            ) + fadeIn(),
+            exit = slideOutHorizontally(
+                targetOffsetX = { if (brightnessIsLeft) -it else it }
+            ) + fadeOut(),
+            modifier = Modifier.align(
+                if (brightnessIsLeft) Alignment.CenterStart else Alignment.CenterEnd
+            ),
+        ) {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(
+                        start = if (brightnessIsLeft) 8.dp else 0.dp,
+                        end = if (brightnessIsLeft) 0.dp else 8.dp,
+                    ),
+                contentAlignment = if (brightnessIsLeft) Alignment.CenterStart else Alignment.CenterEnd,
+            ) {
+                Surface(
+                    modifier = if (useBrightnessHaze && hazeState != null) {
+                        Modifier.readMenuBottomBarHazeEffect(
+                            state = hazeState,
+                            colors = menuColors,
+                            shape = brightnessShape,
+                            menuConfig = state.menuConfig,
+                            progressive = false,
+                        )
+                    } else {
+                        Modifier
+                    },
+                    shape = brightnessShape,
+                    color = if (useBrightnessHaze) {
+                        Color.Transparent
+                    } else {
+                        menuColors.background.copy(
+                            alpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
+                        )
+                    },
+                    contentColor = LegadoTheme.colorScheme.onSurface,
+                ) {
+                    BrightnessBar(
+                        brightness = state.menuConfig.readBrightness,
+                        onBrightnessChange = { value ->
+                            onIntent(ReadBookIntent.SetBrightness(value))
+                        },
+                        brightnessAuto = state.menuConfig.brightnessAuto,
+                        onToggleAuto = {
+                            onIntent(ReadBookIntent.ToggleBrightnessAuto(!state.menuConfig.brightnessAuto))
+                        },
+                        onTogglePosition = {
+                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BrightnessVwPos(if (brightnessIsLeft) "1" else "0")))
+                        },
+                        vertical = true,
+                        colors = menuColors,
+                        menuConfig = state.menuConfig,
+                        backdrop = backdrop,
+                        buttonGlassEnabled = readMenuBottomBarButtonLiquidGlassEnabled(
+                            backdrop = backdrop,
+                            menuConfig = state.menuConfig,
+                        ),
+                        glassThumbEnabled = false,
+                    )
+                }
+            }
+        }
+
         // Bottom menu + floating icon row (bottom positions)
         AnimatedVisibility(
-            visible = state.menuVisible,
+            visible = state.menuVisible || searchMenuVisible,
             enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
             exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
             modifier = Modifier.align(Alignment.BottomCenter),
         ) {
             Column(horizontalAlignment = Alignment.CenterHorizontally) {
-                if (state.menuConfig.showTitleBarIcons && state.menuConfig.titleBarIconPosition >= 2) {
+                if (state.menuVisible &&
+                    state.menuConfig.showTitleBarIcons &&
+                    state.menuConfig.titleBarIconPosition >= 2
+                ) {
                     FloatingIconRow(
                         state = state,
                         colors = menuColors,
@@ -240,29 +355,42 @@ fun ReadBookMenuBar(
                     )
                 }
                 ReadBookMenuSurface(
-                    route = currentRoute,
+                    contentTarget = contentTarget,
                     state = state,
                     colors = menuColors,
                     onIntent = onIntent,
                     context = context,
                     backdrop = backdrop,
                     hazeState = hazeState,
+                    readStylePage = readStylePage,
+                    onReadStylePageChanged = { readStylePage = it },
                 )
             }
         }
     }
 }
 
+private sealed interface ReadBookMenuContent {
+    data object Search : ReadBookMenuContent
+    data class Route(val route: ReadBookMenuRoute) : ReadBookMenuContent
+}
+
 @Composable
 private fun ReadBookMenuSurface(
-    route: ReadBookMenuRoute,
+    contentTarget: ReadBookMenuContent,
     state: ReadBookUiState,
     colors: ReadMenuColors,
     onIntent: (ReadBookIntent) -> Unit,
     context: Context,
     backdrop: Backdrop?,
     hazeState: HazeState?,
+    readStylePage: Int,
+    onReadStylePageChanged: (Int) -> Unit,
 ) {
+    val route = when (contentTarget) {
+        ReadBookMenuContent.Search -> ReadBookMenuRoute.Main
+        is ReadBookMenuContent.Route -> contentTarget.route
+    }
     val expanded = route != ReadBookMenuRoute.Main
     val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig
     val density = LocalDensity.current
@@ -272,6 +400,18 @@ private fun ReadBookMenuSurface(
         targetValue = if (dialogLikeRoute) 1f else 0f,
         label = "ReadBookMenuMorph",
     )
+    val headerFooterLift by animateDpAsState(
+        targetValue = if (route == ReadBookMenuRoute.ReadStyle && readStylePage == 2) {
+            72.dp
+        } else {
+            0.dp
+        },
+        animationSpec = tween(
+            durationMillis = 280,
+            easing = LinearOutSlowInEasing,
+        ),
+        label = "ReadBookMenuHeaderFooterLift",
+    )
     val maxHeight = with(density) {
         windowSize.height.toDp() * 0.64f
     }
@@ -377,7 +517,8 @@ private fun ReadBookMenuSurface(
             .heightIn(max = maxHeight)
             .onSizeChanged { surfaceHeightPx = it.height }
             .offset {
-                val liftPx = ((windowSize.height - surfaceHeightPx) / 2f) * morphProgress
+                val dialogLiftPx = ((windowSize.height - surfaceHeightPx) / 2f) * morphProgress
+                val liftPx = dialogLiftPx + headerFooterLift.toPx()
                 IntOffset(x = 0, y = -liftPx.roundToInt())
             }
             .then(
@@ -433,16 +574,27 @@ private fun ReadBookMenuSurface(
         contentColor = colors.content
     ) {
         AnimatedContent(
-            targetState = route,
+            targetState = contentTarget,
             transitionSpec = {
                 (slideInVertically { it / 4 } + fadeIn())
                     .togetherWith(slideOutVertically { -it / 4 } + fadeOut())
                     .using(SizeTransform(clip = true))
             },
             label = "ReadBookMenuRoute",
-        ) { targetRoute ->
-            when (targetRoute) {
-                ReadBookMenuRoute.Main -> {
+        ) { target ->
+            when (target) {
+                ReadBookMenuContent.Search -> {
+                    SearchBottomMenuContent(
+                        state = state,
+                        colors = colors,
+                        onIntent = onIntent,
+                        bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight + 16.dp else 16.dp,
+                        surfaceEffectEnabled = useLiquidGlass || useHaze,
+                    )
+                }
+
+                is ReadBookMenuContent.Route -> when (val targetRoute = target.route) {
+                    ReadBookMenuRoute.Main -> {
                     MenuBottomBar(
                         state = state,
                         colors = colors,
@@ -482,6 +634,7 @@ private fun ReadBookMenuSurface(
                                 onToggleDayNight = {
                                     onIntent(ReadBookIntent.ToggleDayNight)
                                 },
+                                onPageChanged = onReadStylePageChanged,
                                 readMenuCustomIcons = state.menuConfig.readMenuCustomIcons,
                                 bottomBarButtons = state.menuConfig.bottomBarButtons,
                                 onIntent = onIntent,
@@ -597,6 +750,7 @@ private fun ReadBookMenuSurface(
                             }
                         }
                     }
+                }
             }
         }
     }
@@ -663,7 +817,7 @@ private fun MenuTitleBar(
     backdrop: Backdrop?,
     hazeState: HazeState?,
 ) {
-    val titleBarMode = AppConfig.titleBarMode
+    val titleBarMode = ReadConfig.titleBarMode
 
     var expanded by remember { mutableStateOf(false) }
 
@@ -1404,6 +1558,159 @@ private fun OverflowDropdownMenu(
     }
 }
 
+@Composable
+private fun SearchBottomMenuContent(
+    state: ReadBookUiState,
+    colors: ReadMenuColors,
+    onIntent: (ReadBookIntent) -> Unit,
+    bottomPadding: Dp = 0.dp,
+    surfaceEffectEnabled: Boolean = false,
+) {
+    val totalResults = state.searchResultList.size
+    val currentIndex = state.searchResultIndex.coerceIn(0, (totalResults - 1).coerceAtLeast(0))
+    val percent = if (totalResults > 0) {
+        ((currentIndex + 1) * 100 / totalResults)
+    } else {
+        0
+    }
+
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .background(
+                if (surfaceEffectEnabled) Color.Transparent else colors.background.copy(
+                    alpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
+                )
+            )
+            .windowInsetsPadding(
+                WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)
+            )
+            .padding(top = 12.dp, bottom = bottomPadding)
+            .animateContentSize(),
+    ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            SearchInfoPill(
+                modifier = Modifier.weight(0.7f),
+            ) {
+                Text(
+                    text = if (totalResults > 0) "${currentIndex + 1} / $totalResults" else "0 / 0",
+                    style = LegadoTheme.typography.titleSmall,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 1,
+                )
+                Spacer(Modifier.width(8.dp))
+                Text(
+                    text = "$percent%",
+                    style = LegadoTheme.typography.bodySmall,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 1,
+                )
+            }
+
+            SearchInfoPill(
+                modifier = Modifier.weight(1.3f),
+            ) {
+                Text(
+                    text = state.chapterName.ifBlank { "-" },
+                    style = LegadoTheme.typography.bodyMedium,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                )
+            }
+        }
+
+        Spacer(Modifier.height(8.dp))
+
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(horizontal = 12.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            SearchMenuActionButton(
+                modifier = Modifier.weight(1f),
+                icon = Icons.Default.Search,
+                text = "搜索内容",
+                onClick = { onIntent(ReadBookIntent.OpenSearch(null)) },
+            )
+            SearchMenuActionButton(
+                modifier = Modifier.weight(1f),
+                icon = Icons.Default.Menu,
+                text = "主菜单",
+                onClick = {
+                    onIntent(ReadBookIntent.HideSearchMenu)
+                    onIntent(ReadBookIntent.ShowMenu)
+                },
+            )
+            SearchMenuActionButton(
+                modifier = Modifier.weight(0.55f),
+                icon = Icons.Default.Close,
+                text = "退出",
+                onClick = { onIntent(ReadBookIntent.ExitSearch) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun SearchInfoPill(
+    modifier: Modifier = Modifier,
+    content: @Composable RowScope.() -> Unit,
+) {
+    Row(
+        modifier = modifier
+            .height(40.dp)
+            .clip(RoundedCornerShape(16.dp))
+            .background(LegadoTheme.colorScheme.surfaceContainerLow)
+            .padding(horizontal = 12.dp),
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.Center,
+        content = content,
+    )
+}
+
+@Composable
+private fun SearchMenuActionButton(
+    modifier: Modifier = Modifier,
+    icon: ImageVector,
+    text: String,
+    onClick: () -> Unit,
+) {
+    Row(
+        modifier = modifier
+            .height(44.dp)
+            .clip(RoundedCornerShape(16.dp))
+            .background(LegadoTheme.colorScheme.surfaceContainerLow)
+            .clickable(role = Role.Button, onClick = onClick)
+            .padding(horizontal = 10.dp),
+        horizontalArrangement = Arrangement.Center,
+        verticalAlignment = Alignment.CenterVertically,
+    ) {
+        Icon(
+            imageVector = icon,
+            contentDescription = null,
+            tint = LegadoTheme.colorScheme.primary,
+            modifier = Modifier.size(18.dp),
+        )
+        Spacer(Modifier.width(6.dp))
+        Text(
+            text = text,
+            style = LegadoTheme.typography.labelMediumEmphasized,
+            color = LegadoTheme.colorScheme.onSurfaceVariant,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
+        )
+    }
+}
+
 @Composable
 private fun MenuBottomBar(
     state: ReadBookUiState,
@@ -1431,7 +1738,7 @@ private fun MenuBottomBar(
         val target = value.roundToInt().coerceIn(0, seekMax)
         sliderDragging = false
         sliderValue = target.toFloat()
-        val behavior = AppConfig.progressBarBehavior
+        val behavior = ReadConfig.progressBarBehavior
         if (behavior == "page") {
             onIntent(ReadBookIntent.SkipToPage(target))
         } else {
@@ -1459,6 +1766,27 @@ private fun MenuBottomBar(
             .padding(top = 8.dp, bottom = contentBottomPadding)
             .animateContentSize(),
     ) {
+        if (state.menuConfig.showBrightnessView == "1") {
+            BrightnessBar(
+                brightness = state.menuConfig.readBrightness,
+                onBrightnessChange = { value ->
+                    onIntent(ReadBookIntent.SetBrightness(value))
+                },
+                brightnessAuto = state.menuConfig.brightnessAuto,
+                onToggleAuto = {
+                    onIntent(ReadBookIntent.ToggleBrightnessAuto(!state.menuConfig.brightnessAuto))
+                },
+                onTogglePosition = {},
+                vertical = false,
+                colors = colors,
+                menuConfig = state.menuConfig,
+                backdrop = backdrop,
+                buttonGlassEnabled = buttonGlassEnabled,
+                glassThumbEnabled = buttonGlassEnabled,
+            )
+            Spacer(Modifier.height(4.dp))
+        }
+
         // Seek bar row: prev + slider + next
         AnimatedVisibility(visible = state.menuConfig.readSliderMode != "1") {
             Row(
@@ -2181,8 +2509,8 @@ private fun Modifier.readMenuLiquidGlass(
             }
         },
     )
-        .then(if (interactiveHighlight != null) interactiveHighlight.modifier else Modifier)
-        .then(if (interactiveHighlight != null) interactiveHighlight.gestureModifier else Modifier)
+        .then(interactiveHighlight?.modifier ?: Modifier)
+        .then(interactiveHighlight?.gestureModifier ?: Modifier)
 }
 
 @OptIn(ExperimentalHazeMaterialsApi::class)
@@ -2288,7 +2616,7 @@ private fun readMenuBottomBarSurfaceBrush(
 private fun readMenuColors(): ReadMenuColors {
     val themeBackground = LegadoTheme.colorScheme.surfaceContainerHigh
     val themeContent = LegadoTheme.colorScheme.onSurface
-    return when (AppConfig.readBarStyle) {
+    return when (ReadConfig.readBarStyle) {
         1 -> ReadMenuColors(
             background = themeBackground,
             content = themeContent,
@@ -2370,3 +2698,116 @@ private fun loadFloatingIcons(
         }
         .toList()
 }
+
+@Composable
+private fun BrightnessBar(
+    brightness: Int,
+    onBrightnessChange: (Int) -> Unit,
+    brightnessAuto: Boolean,
+    onToggleAuto: () -> Unit,
+    onTogglePosition: () -> Unit,
+    modifier: Modifier = Modifier,
+    vertical: Boolean = false,
+    colors: ReadMenuColors,
+    menuConfig: ReadMenuConfig,
+    backdrop: Backdrop? = null,
+    buttonGlassEnabled: Boolean = false,
+    glassThumbEnabled: Boolean = false,
+) {
+    var sliderValue by remember { mutableFloatStateOf(brightness.toFloat()) }
+    var sliderDragging by remember { mutableStateOf(false) }
+
+    LaunchedEffect(brightness) {
+        if (!sliderDragging) {
+            sliderValue = brightness.toFloat()
+        }
+    }
+
+    fun commitSliderValue(value: Float) {
+        val target = value.roundToInt().coerceIn(0, 100)
+        sliderDragging = false
+        sliderValue = target.toFloat()
+        onBrightnessChange(target)
+    }
+
+    if (vertical) {
+        Column(
+            modifier = modifier
+                .width(if (buttonGlassEnabled) 64.dp else 56.dp)
+                .padding(vertical = 12.dp, horizontal = 4.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(10.dp),
+        ) {
+            ReadMenuGlassIconButton(
+                onClick = onToggleAuto,
+                icon = Icons.Filled.BrightnessAuto,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                selected = brightnessAuto,
+                contentDescription = stringResource(R.string.brightness_follow_system),
+            )
+            AppVerticalSlider(
+                value = sliderValue.coerceIn(0f, 100f),
+                onValueChange = { value ->
+                    sliderDragging = true
+                    sliderValue = value.coerceIn(0f, 100f)
+                },
+                onValueChangeFinished = {
+                    commitSliderValue(sliderValue)
+                },
+                valueRange = 0f..100f,
+                enabled = !brightnessAuto,
+                height = 168.dp,
+            )
+            ReadMenuGlassIconButton(
+                onClick = onTogglePosition,
+                icon = Icons.Filled.SwapHoriz,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                contentDescription = stringResource(R.string.brightness_bar_position),
+            )
+        }
+    } else {
+        val buttonSize = if (buttonGlassEnabled) 48.dp else 40.dp
+        Row(
+            modifier = modifier
+                .fillMaxWidth()
+                .padding(horizontal = 16.dp, vertical = 4.dp),
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+        ) {
+            ReadMenuGlassIconButton(
+                onClick = onToggleAuto,
+                icon = Icons.Filled.BrightnessAuto,
+                colors = colors,
+                backdrop = backdrop,
+                menuConfig = menuConfig,
+                glassEnabled = buttonGlassEnabled,
+                selected = brightnessAuto,
+                contentDescription = stringResource(R.string.brightness_follow_system),
+            )
+            ReadMenuSlider(
+                value = sliderValue.coerceIn(0f, 100f),
+                onValueChange = { value ->
+                    sliderDragging = true
+                    sliderValue = value.coerceIn(0f, 100f)
+                },
+                onValueChangeFinished = {
+                    commitSliderValue(sliderValue)
+                },
+                valueRange = 0f..100f,
+                enabled = !brightnessAuto,
+                backdrop = backdrop,
+                glassThumbEnabled = glassThumbEnabled,
+                modifier = Modifier
+                    .weight(1f)
+                    .padding(horizontal = 8.dp),
+            )
+            Spacer(Modifier.width(buttonSize))
+        }
+    }
+}

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

@@ -27,14 +27,12 @@ import dev.chrisbanes.haze.hazeSource
 import io.legado.app.R
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.ReadMenuBlurMode
-import io.legado.app.help.IntentData
 import io.legado.app.help.IntentHelp
 import io.legado.app.ui.book.info.BookInfoActivity
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.ReadView
 import io.legado.app.ui.book.read.page.entities.PageDirection
-import io.legado.app.ui.book.searchContent.SearchContentActivity
-import io.legado.app.ui.book.searchContent.SearchResult
+import io.legado.app.ui.book.searchContent.SearchContentResult
 import io.legado.app.ui.book.source.edit.BookSourceEditActivity
 import io.legado.app.ui.book.toc.TocActivityResult
 import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
@@ -45,6 +43,7 @@ import io.legado.app.ui.replace.ReplaceRuleActivity
 import io.legado.app.utils.StartActivityContract
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.flow.onSubscription
 import kotlinx.coroutines.launch
 
@@ -95,10 +94,12 @@ fun ReadBookRouteScreen(
     host: ReadBookRouteHost,
     controller: ReadBookController,
     onEffectsReady: () -> Unit = {},
+    onOpenSearch: (word: String?, bookUrl: String) -> Unit = { _, _ -> },
 ) {
     val state by viewModel.uiState.collectAsStateWithLifecycle()
     val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
     val context = LocalContext.current
+    val effectsReady = remember(viewModel) { CompletableDeferred<Unit>() }
     val menuBackdrop = rememberLayerBackdrop()
     val menuHazeState = remember { HazeState() }
     val useMenuHazeSource = state.menuConfig.readMenuTopBarBlurMode == ReadMenuBlurMode.Haze ||
@@ -209,19 +210,6 @@ fun ReadBookRouteScreen(
         }
     }
 
-    val searchContentLauncher = rememberLauncherForActivityResult(
-        StartActivityContract(SearchContentActivity::class.java)
-    ) { result ->
-        val data = result.data ?: return@rememberLauncherForActivityResult
-        val key = data.getLongExtra("key", System.currentTimeMillis())
-        val index = data.getIntExtra("index", 0)
-        val searchResult = IntentData.get<SearchResult>("searchResult$key")
-        val searchResultList = IntentData.get<List<SearchResult>>("searchResultList$key")
-        if (searchResult != null && searchResultList != null) {
-            viewModel.onIntent(ReadBookIntent.SetSearchResults(searchResultList, index, searchResult.query))
-        }
-    }
-
     val importHttpTtsPicker = rememberLauncherForActivityResult(
         ActivityResultContracts.OpenDocument()
     ) { uri ->
@@ -245,7 +233,10 @@ fun ReadBookRouteScreen(
     LaunchedEffect(viewModel) {
         launch {
             viewModel.effects
-                .onSubscription { onEffectsReady() }
+                .onSubscription {
+                    effectsReady.complete(Unit)
+                    onEffectsReady()
+                }
                 .collect { effect ->
                     try {
                         when (effect) {
@@ -292,18 +283,7 @@ fun ReadBookRouteScreen(
                                 )
                             }
                             is ReadBookEffect.OpenSearchActivity -> {
-                                val currentState = viewModel.uiState.value
-                                val lambda: (Intent.() -> Unit)? = { intent ->
-                                    intent.putExtra("bookUrl", effect.bookUrl)
-                                    intent.putExtra("searchWord", effect.word)
-                                    intent.putExtra("searchResultIndex", currentState.searchResultIndex)
-                                    currentState.searchResultList.firstOrNull()?.let {
-                                        if (it.query == currentState.searchContentQuery) {
-                                            IntentData.put("searchResultList", currentState.searchResultList)
-                                        }
-                                    }
-                                }
-                                searchContentLauncher.launch(lambda)
+                                onOpenSearch(effect.word, effect.bookUrl)
                             }
                             is ReadBookEffect.MenuSettingReplace -> {
                                 replaceLauncher.launch(Intent(context, ReplaceRuleActivity::class.java))
@@ -392,6 +372,24 @@ fun ReadBookRouteScreen(
     LaunchedEffect(state.menuVisible) {
         host.upSystemUiVisibility(host.isInMultiWindowModeCompat, !state.menuVisible)
     }
+
+    // ── Search result collection (from Navigation3 search route) ──────
+
+    LaunchedEffect(viewModel) {
+        SearchContentResult.results.collect { result ->
+            effectsReady.await()
+            viewModel.onIntent(
+                ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query)
+            )
+            result.searchResults.getOrNull(result.index)?.let { searchResult ->
+                viewModel.onIntent(
+                    ReadBookIntent.NavigateToSearchResult(searchResult, result.index)
+                )
+            }
+            SearchContentResult.resetReplayCache()
+        }
+    }
+
     // ── View layer + Compose UI ───────────────────────────────────────
 
     Box(Modifier.fillMaxSize()) {

+ 5 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -23,7 +23,6 @@ import io.legado.app.ui.book.read.sheet.ContentEditSheet
 import io.legado.app.ui.book.read.sheet.DictSheet
 import io.legado.app.ui.book.read.sheet.DownloadSheet
 import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
-import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
 import io.legado.app.ui.book.read.sheet.HttpTtsEditSheet
 import io.legado.app.ui.book.read.sheet.MoreConfigSheet
@@ -38,6 +37,7 @@ import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
 import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
 import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
 import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
+import io.legado.app.ui.widget.components.FontSelectSheet
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
 import io.legado.app.ui.widget.components.log.AppLogSheet
@@ -135,6 +135,10 @@ fun ReadBookScreen(
             onIntent(ReadBookIntent.OpenReplaceEditor(id, pattern))
         },
         onReplaceRuleChanged = { onIntent(ReadBookIntent.ReplaceRuleChanged) },
+        onNavigateToTextEffects = {
+            onIntent(ReadBookIntent.DismissSheet)
+            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TextTitle))
+        },
     )
     UnderlineConfigSheet(
         show = state.activeSheet is ReadBookSheet.UnderlineConfig,

+ 13 - 163
app/src/main/java/io/legado/app/ui/book/read/ReadBookSearchBar.kt

@@ -3,44 +3,23 @@ package io.legado.app.ui.book.read
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
 import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.SmallFloatingActionButton
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.AppFloatingActionButton
 
 /**
- * Compose replacement for SearchMenu — search result navigation overlay.
+ * Search result navigation overlay. The bottom search menu is hosted by [ReadBookMenuBar].
  */
 @Composable
 fun ReadBookSearchBar(
@@ -52,9 +31,6 @@ fun ReadBookSearchBar(
     val hasResults = state.searchResultList.isNotEmpty()
     val currentIndex = state.searchResultIndex
     val totalResults = state.searchResultList.size
-    val currentResult = if (hasResults && currentIndex in state.searchResultList.indices) {
-        state.searchResultList[currentIndex]
-    } else null
 
     Box(Modifier.fillMaxSize()) {
         // Left FAB - previous result
@@ -66,7 +42,7 @@ fun ReadBookSearchBar(
                 .align(Alignment.CenterStart)
                 .padding(start = 16.dp),
         ) {
-            SmallFloatingActionButton(
+            AppFloatingActionButton(
                 onClick = {
                     val prevIndex = currentIndex - 1
                     onIntent(
@@ -75,11 +51,13 @@ fun ReadBookSearchBar(
                         )
                     )
                 },
-                containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
+                tooltipText = "上一个结果",
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
             ) {
                 Icon(
                     Icons.AutoMirrored.Filled.ArrowBack,
-                    contentDescription = "Previous",
+                    contentDescription = "上一个结果",
                     modifier = Modifier.size(20.dp),
                 )
             }
@@ -94,7 +72,7 @@ fun ReadBookSearchBar(
                 .align(Alignment.CenterEnd)
                 .padding(end = 16.dp),
         ) {
-            SmallFloatingActionButton(
+            AppFloatingActionButton(
                 onClick = {
                     val nextIndex = currentIndex + 1
                     onIntent(
@@ -103,144 +81,16 @@ fun ReadBookSearchBar(
                         )
                     )
                 },
-                containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
+                tooltipText = "下一个结果",
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                contentColor = LegadoTheme.colorScheme.onSurfaceVariant,
             ) {
                 Icon(
                     Icons.AutoMirrored.Filled.ArrowForward,
-                    contentDescription = "Next",
+                    contentDescription = "下一个结果",
                     modifier = Modifier.size(20.dp),
                 )
             }
         }
-
-        // Tap background to dismiss search menu
-        AnimatedVisibility(
-            visible = searchVisible && state.searchMenuVisible,
-            enter = fadeIn(),
-            exit = fadeOut(),
-        ) {
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .clickable(
-                        indication = null,
-                        interactionSource = remember { MutableInteractionSource() },
-                    ) { onIntent(ReadBookIntent.HideSearchMenu) }
-            )
-        }
-
-        // Bottom menu
-        AnimatedVisibility(
-            visible = searchVisible && state.searchMenuVisible,
-            enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
-            exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
-            modifier = Modifier.align(Alignment.BottomCenter),
-        ) {
-            SearchBottomMenu(
-                state = state,
-                currentResult = currentResult,
-                onIntent = onIntent,
-            )
-        }
-    }
-}
-
-@Composable
-private fun SearchBottomMenu(
-    state: ReadBookUiState,
-    currentResult: io.legado.app.ui.book.searchContent.SearchResult?,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    Column(
-        modifier = Modifier
-            .fillMaxWidth()
-            .background(MaterialTheme.colorScheme.surfaceContainer)
-            .padding(bottom = 16.dp),
-    ) {
-        // Search progress info
-        Row(
-            modifier = Modifier
-                .fillMaxWidth()
-                .padding(horizontal = 16.dp, vertical = 8.dp),
-            horizontalArrangement = Arrangement.SpaceBetween,
-            verticalAlignment = Alignment.CenterVertically,
-        ) {
-            // Fraction: "3 / 10"
-            Card(
-                colors = CardDefaults.cardColors(
-                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
-                    contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
-                ),
-            ) {
-                Row(
-                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
-                    verticalAlignment = Alignment.CenterVertically,
-                ) {
-                    Text(
-                        text = "${state.searchResultIndex + 1} / ${state.searchResultList.size}",
-                        style = MaterialTheme.typography.titleSmall,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
-                    )
-                    Spacer(Modifier.width(8.dp))
-                    val percent = if (state.searchResultList.isNotEmpty()) {
-                        ((state.searchResultIndex + 1) * 100 / state.searchResultList.size)
-                    } else 0
-                    Text(
-                        text = "$percent%",
-                        style = MaterialTheme.typography.bodySmall,
-                        color = MaterialTheme.colorScheme.onSurfaceVariant,
-                    )
-                }
-            }
-
-            // Current chapter
-            Card(
-                colors = CardDefaults.cardColors(
-                    containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
-                    contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
-                ),
-            ) {
-                Text(
-                    text = state.chapterName,
-                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
-                    style = MaterialTheme.typography.bodyMedium,
-                    maxLines = 1,
-                )
-            }
-        }
-
-        Spacer(Modifier.height(4.dp))
-
-        // Action buttons
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = Arrangement.SpaceEvenly,
-        ) {
-            TextButton(
-                onClick = { onIntent(ReadBookIntent.OpenSearch(null)) },
-                modifier = Modifier.weight(1f),
-            ) {
-                Icon(
-                    Icons.Default.Search,
-                    contentDescription = null,
-                    modifier = Modifier.size(18.dp)
-                )
-                Spacer(Modifier.width(4.dp))
-                Text("搜索内容")
-            }
-            TextButton(
-                onClick = { onIntent(ReadBookIntent.ShowMenu) },
-                modifier = Modifier.weight(1f),
-            ) {
-                Icon(Icons.Default.Menu, contentDescription = null, modifier = Modifier.size(18.dp))
-                Spacer(Modifier.width(4.dp))
-                Text("主菜单")
-            }
-            IconButton(
-                onClick = { onIntent(ReadBookIntent.ExitSearch) },
-            ) {
-                Icon(Icons.Default.Close, contentDescription = "Exit search")
-            }
-        }
     }
 }

+ 141 - 40
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -35,7 +35,6 @@ import io.legado.app.domain.usecase.GetReadingProgressUseCase
 import io.legado.app.domain.usecase.UploadReadingProgressUseCase
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.DefaultData
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.isEpub
@@ -45,7 +44,6 @@ import io.legado.app.help.book.isLocalTxt
 import io.legado.app.help.book.isMobi
 import io.legado.app.help.book.removeType
 import io.legado.app.help.book.simulatedTotalChapterNum
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.source.getSourceType
@@ -66,6 +64,7 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.TextChapterLayout
 import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.ui.config.otherConfig.OtherConfig
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.ImageSaveUtils
@@ -78,7 +77,6 @@ import io.legado.app.utils.isTrue
 import io.legado.app.utils.mapParallelSafe
 import io.legado.app.utils.openUrl
 import io.legado.app.utils.postEvent
-import io.legado.app.utils.putPrefInt
 import io.legado.app.utils.toStringArray
 import io.legado.app.utils.toastOnUi
 import kotlinx.collections.immutable.toImmutableList
@@ -155,7 +153,7 @@ class ReadBookViewModel(
     }
 
     init {
-        AppConfig.detectClickArea()
+        ReadConfig.detectClickArea()
         ReadBook.register(this)
         refreshButtonConfigs()
         collectReadPreferences()
@@ -253,7 +251,7 @@ class ReadBookViewModel(
 
             is ReadBookIntent.NavigateToSearchResult -> {
                 _uiState.update { it.copy(searchResultIndex = intent.index) }
-                _effects.tryEmit(ReadBookEffect.NavigateToSearchResult(intent.result))
+                navigateToSearchResult(intent.result)
             }
 
             is ReadBookIntent.ToggleReadAloud -> {
@@ -296,8 +294,28 @@ class ReadBookViewModel(
             is ReadBookIntent.CancelSelect -> _effects.tryEmit(ReadBookEffect.CancelSelect)
             is ReadBookIntent.UpSystemUiVisibility -> _effects.tryEmit(ReadBookEffect.UpSystemUiVisibility)
             is ReadBookIntent.UpContent -> ReadBook.loadOrUpContent()
-            is ReadBookIntent.SetBrightness -> _effects.tryEmit(ReadBookEffect.SetBrightness(intent.value))
-            is ReadBookIntent.ToggleBrightnessAuto -> _effects.tryEmit(ReadBookEffect.ToggleBrightnessAuto)
+            is ReadBookIntent.SetBrightness -> {
+                ReadBookConfig.readBrightness = intent.value
+                _uiState.update { it.copy(menuConfig = it.menuConfig.copy(readBrightness = intent.value)) }
+                viewModelScope.launch {
+                    readSettingsRepository.setReadBrightness(intent.value)
+                }
+                _effects.tryEmit(ReadBookEffect.SetBrightness(intent.value))
+            }
+
+            is ReadBookIntent.ToggleBrightnessAuto -> {
+                ReadBookConfig.brightnessAuto = intent.auto
+                _uiState.update { it.copy(menuConfig = it.menuConfig.copy(brightnessAuto = intent.auto)) }
+                viewModelScope.launch {
+                    readSettingsRepository.setBrightnessAuto(intent.auto)
+                }
+                _effects.tryEmit(
+                    ReadBookEffect.ToggleBrightnessAuto(
+                        intent.auto,
+                        _uiState.value.menuConfig.readBrightness
+                    )
+                )
+            }
             is ReadBookIntent.SeekToChapter -> {
                 ReadBook.saveCurrentBookProgress()
                 openChapter(intent.index)
@@ -627,7 +645,7 @@ class ReadBookViewModel(
             is ReadBookIntent.OpenPreDownloadNumPicker -> {
                 _uiState.update {
                     it.copy(
-                        preDownloadNum = AppConfig.preDownloadNum,
+                        preDownloadNum = ReadConfig.preDownloadNum,
                         activeSheet = ReadBookSheet.PreDownloadConfig,
                     )
                 }
@@ -636,7 +654,7 @@ class ReadBookViewModel(
             is ReadBookIntent.OpenCacheCleanTimePicker -> {
                 _uiState.update {
                     it.copy(
-                        audioCacheCleanTime = AppConfig.audioCacheCleanTimeOrgin,
+                        audioCacheCleanTime = ReadConfig.audioCacheCleanTimeOrgin,
                         activeSheet = ReadBookSheet.AudioCacheCleanConfig,
                     )
                 }
@@ -644,7 +662,8 @@ class ReadBookViewModel(
 
             is ReadBookIntent.ApplySpeakEngine -> {
                 ReadBook.book?.setTtsEngine(null)
-                AppConfig.ttsEngine = intent.value
+                ReadConfig.ttsEngine = intent.value
+                ReadAloud.upReadAloudClass()
                 _uiState.update {
                     it.copy(
                         selectedTtsEngine = ReadAloud.ttsEngine,
@@ -655,7 +674,7 @@ class ReadBookViewModel(
             }
 
             is ReadBookIntent.ApplyPreDownloadNum -> {
-                AppConfig.preDownloadNum = intent.value
+                ReadConfig.preDownloadNum = intent.value
                 _uiState.update {
                     it.copy(
                         preDownloadNum = intent.value,
@@ -665,7 +684,7 @@ class ReadBookViewModel(
             }
 
             is ReadBookIntent.ApplyAudioCacheCleanTime -> {
-                context.putPrefInt(PreferKey.audioCacheCleanTime, intent.value)
+                ReadConfig.audioCacheCleanTimeOrgin = intent.value
                 _uiState.update {
                     it.copy(
                         audioCacheCleanTime = intent.value,
@@ -704,7 +723,7 @@ class ReadBookViewModel(
                 }.onSuccess {
                     loadTtsEngineItems()
                     if (ReadAloud.ttsEngine == intent.engineId.toString()) {
-                        AppConfig.ttsEngine = null
+                        ReadConfig.ttsEngine = null
                         ReadAloud.upReadAloudClass()
                     }
                 }
@@ -726,6 +745,7 @@ class ReadBookViewModel(
 
             is ReadBookIntent.ApplySpeakEnginePerBook -> {
                 ReadBook.book?.setTtsEngine(intent.value)
+                ReadAloud.upReadAloudClass()
                 _uiState.update {
                     it.copy(
                         selectedTtsEngine = ReadAloud.ttsEngine,
@@ -841,7 +861,7 @@ class ReadBookViewModel(
 
             is ReadBookIntent.SelectFont -> selectFont(intent.path)
             is ReadBookIntent.SelectSystemTypeface -> {
-                AppConfig.systemTypefaces = intent.index
+                ReadConfig.systemTypefaces = intent.index
                 ReadBookConfig.textFont = ""
                 _effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
                     setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.ReloadContent)
@@ -937,7 +957,7 @@ class ReadBookViewModel(
             is ReadBookIntent.ToggleDayNight -> toggleDayNight()
             // Text action menu
             is ReadBookIntent.TextActionAloud -> {
-                when (AppConfig.contentSelectSpeakMod) {
+                when (ReadConfig.contentSelectSpeakMod) {
                     1 -> _effects.tryEmit(ReadBookEffect.TextActionAloudSelect)
                     else -> _effects.tryEmit(ReadBookEffect.TextActionSpeak(intent.text))
                 }
@@ -1052,7 +1072,7 @@ class ReadBookViewModel(
         _effects.tryEmit(ReadBookEffect.UnregisterNetworkListener)
 
         if (!BuildConfig.DEBUG) {
-            if (AppConfig.syncBookProgressPlus) {
+            if (ReadConfig.syncBookProgressPlus) {
                 ReadBook.syncProgress()
             } else {
                 ReadBook.uploadProgress()
@@ -1123,7 +1143,7 @@ class ReadBookViewModel(
      * Called from the network changed listener (registered by route).
      */
     fun onNetworkChanged() {
-        if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) {
+        if (ReadConfig.syncBookProgressPlus && NetworkUtils.isAvailable() && !justInitData) {
             ReadBook.syncProgress(newProgressAction = { progress ->
                 sureNewProgress(progress)
             })
@@ -1149,7 +1169,7 @@ class ReadBookViewModel(
 
     private fun handleChangeSource() {
         viewModelScope.launch {
-            if (AppConfig.defaultSourceChangeAll) {
+            if (ReadConfig.defaultSourceChangeAll) {
                 _uiState.update { it.copy(activeSheet = ReadBookSheet.ChangeBookSource) }
             } else {
                 val book = ReadBook.book ?: return@launch
@@ -1393,10 +1413,10 @@ class ReadBookViewModel(
             readSettingsRepository.preferences.collect { preferences ->
                 val old = previous
                 previous = preferences
-                AppConfig.syncReadPreferences(preferences)
+                ReadConfig.syncReadPreferences(preferences)
                 _readPreferences.value = preferences
                 if (!preferences.hasMenuClickArea()) {
-                    AppConfig.detectClickArea()
+                    ReadConfig.detectClickArea()
                     readSettingsRepository.setClickAction(PreferKey.clickActionMC, 0)
                 }
                 if (old != null && old.keepLight != preferences.keepLight) {
@@ -1423,11 +1443,7 @@ class ReadBookViewModel(
                         readAloudStreamAudio = prefs.streamReadAloudAudio,
                         readAloudTtsFollowSys = prefs.ttsFollowSys,
                         readAloudTtsSpeechRate = prefs.ttsSpeechRate,
-                        readAloudTtsTimer = if (BaseReadAloudService.timeMinute > 0) {
-                            BaseReadAloudService.timeMinute
-                        } else {
-                            prefs.ttsTimer
-                        },
+                        readAloudTtsTimer = prefs.ttsTimer,
                     )
                 }
             }
@@ -1445,11 +1461,12 @@ class ReadBookViewModel(
     }
 
     private fun setReadAloudTtsTimer(value: Int) {
+        val timer = value.coerceIn(0, 180)
         viewModelScope.launch {
-            readAloudSettingsRepository.setTtsTimer(value)
+            readAloudSettingsRepository.setTtsTimer(timer)
         }
-        ReadAloud.setTimer(context, value)
-        _uiState.update { it.copy(readAloudTtsTimer = value) }
+        ReadAloud.setTimer(context, timer)
+        _uiState.update { it.copy(readAloudTtsTimer = timer) }
     }
 
     private fun setReadAloudTtsSpeechRate(value: Int) {
@@ -1557,6 +1574,10 @@ class ReadBookViewModel(
                 readMenuCustomIcons = ReadBookConfig.readMenuCustomIcons.toImmutableMap(),
                 titleBarButtons = current.menuConfig.titleBarButtons,
                 bottomBarButtons = current.menuConfig.bottomBarButtons,
+                showBrightnessView = ReadBookConfig.showBrightnessView,
+                brightnessVwPos = ReadBookConfig.brightnessVwPos,
+                readBrightness = ReadBookConfig.readBrightness,
+                brightnessAuto = ReadBookConfig.brightnessAuto,
             ),
         )
     }
@@ -1659,14 +1680,14 @@ class ReadBookViewModel(
     }
 
     private fun calculateSeekProgress(): Int {
-        return when (AppConfig.progressBarBehavior) {
+        return when (ReadConfig.progressBarBehavior) {
             "page" -> ReadBook.durPageIndex
             else -> ReadBook.durChapterIndex
         }
     }
 
     private fun calculateSeekMax(): Int {
-        return when (AppConfig.progressBarBehavior) {
+        return when (ReadConfig.progressBarBehavior) {
             "page" -> (ReadBook.curTextChapter?.pages?.size ?: 1) - 1
             else -> ReadBook.chapterSize - 1
         }
@@ -1761,7 +1782,7 @@ class ReadBookViewModel(
         if (ReadBook.chapterChanged) {
             ReadBook.chapterChanged = false
         } else if (!(isSameBook && BaseReadAloudService.isRun) && ReadBook.inBookshelf) {
-            if (AppConfig.syncBookProgressPlus) {
+            if (ReadConfig.syncBookProgressPlus) {
                 ReadBook.syncProgress({ progress -> sureNewProgress(progress) })
             } else {
                 syncBookProgress(book)
@@ -1857,7 +1878,7 @@ class ReadBookViewModel(
         book: Book,
         alertSync: ((progress: BookProgress) -> Unit)? = null
     ) {
-        if (!AppConfig.syncBookProgress) return
+        if (!ReadConfig.syncBookProgress) return
         execute {
             getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
         }.onError {
@@ -1952,7 +1973,7 @@ class ReadBookViewModel(
     }
 
     private fun autoChangeSource(name: String, author: String) {
-        if (!AppConfig.autoChangeSource) return
+        if (!ReadConfig.autoChangeSource) return
         execute {
             val sources = appDb.bookSourceDao.allTextEnabledPart
             flow {
@@ -2164,11 +2185,11 @@ class ReadBookViewModel(
 
     fun searchResultPositions(
         textChapter: TextChapter,
-        searchResult: SearchResult
+        searchResult: SearchResult,
+        query: String = _uiState.value.searchContentQuery,
     ): Array<Int> {
         val pages = textChapter.pages
         val content = textChapter.getContent()
-        val query = _uiState.value.searchContentQuery
         val queryLength = query.length
 
         var count = 0
@@ -2178,6 +2199,7 @@ class ReadBookViewModel(
             count += 1
         }
         val contentPosition = index
+        if (contentPosition < 0) return arrayOf(0, 0, 0, 0, 0)
         var pageIndex = 0
         var length = pages[pageIndex].text.length
         while (length < contentPosition && pageIndex + 1 < pages.size) {
@@ -2191,7 +2213,7 @@ class ReadBookViewModel(
         var curLine = curTextLines[lineIndex]
         length = length - currentPage.text.length + curLine.text.length
         if (curLine.isParagraphEnd) length++
-        while (length <= contentPosition && lineIndex + 1 < curTextLines.size) {
+        while (length < contentPosition && lineIndex + 1 < curTextLines.size) {
             lineIndex += 1
             curLine = curTextLines[lineIndex]
             length += curLine.text.length
@@ -2217,6 +2239,55 @@ class ReadBookViewModel(
         return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2)
     }
 
+    /**
+     * Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult]
+     * so the Controller can navigate and highlight.
+     */
+    private fun navigateToSearchResult(result: SearchResult) {
+        val query = _uiState.value.searchContentQuery
+        if (query.isEmpty()) return
+        val chapterIndex = result.chapterIndex
+        val textChapter = ReadBook.curTextChapter
+        if (textChapter != null && textChapter.chapter.index == chapterIndex) {
+            val pos = searchResultPositions(textChapter, result, query)
+            val lineIndex = pos[1]
+            val charIndex = pos[2]
+            val addLine = pos[3]
+            val charIndex2 = pos[4]
+            val queryLength = query.length
+            val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
+            val endCharIndex = if (addLine == 0) {
+                charIndex + queryLength
+            } else {
+                charIndex2 + 1
+            }
+            _effects.tryEmit(
+                ReadBookEffect.NavigateToSearchResult(
+                    result = result,
+                    chapterIndex = chapterIndex,
+                    pageIndex = pos[0],
+                    lineIndex = lineIndex,
+                    startCharIndex = charIndex,
+                    endLineIndex = endLineIndex,
+                    endCharIndex = endCharIndex,
+                )
+            )
+        } else {
+            // Chapter not loaded — emit with -1 so the Controller knows to open the chapter first
+            _effects.tryEmit(
+                ReadBookEffect.NavigateToSearchResult(
+                    result = result,
+                    chapterIndex = chapterIndex,
+                    pageIndex = -1,
+                    lineIndex = 0,
+                    startCharIndex = 0,
+                    endLineIndex = 0,
+                    endCharIndex = 0,
+                )
+            )
+        }
+    }
+
     fun reverseRemoveSameTitle() {
         execute {
             val book = ReadBook.book ?: return@execute
@@ -2423,7 +2494,7 @@ class ReadBookViewModel(
             }
             is ConfigUpdate.ReadBarStyle -> {
                 val value = update.value.coerceIn(0, 2)
-                AppConfig.updateReadBarStyleCache(value)
+                ReadConfig.readBarStyle = value
                 viewModelScope.launch {
                     readSettingsRepository.setReadBarStyle(value)
                 }
@@ -2760,11 +2831,41 @@ class ReadBookViewModel(
                 }
             }
             is ConfigUpdate.ShowBrightnessView -> {
+                ReadBookConfig.showBrightnessView = update.value
                 viewModelScope.launch {
                     readSettingsRepository.setShowBrightnessView(update.value)
                 }
+                _uiState.update {
+                    it.copy(menuConfig = it.menuConfig.copy(showBrightnessView = update.value))
+                }
                 postEvent(PreferKey.showBrightnessView, "")
             }
+
+            is ConfigUpdate.BrightnessVwPos -> {
+                ReadBookConfig.brightnessVwPos = update.value
+                viewModelScope.launch {
+                    readSettingsRepository.setBrightnessVwPos(update.value)
+                }
+                _uiState.update {
+                    it.copy(menuConfig = it.menuConfig.copy(brightnessVwPos = update.value))
+                }
+            }
+
+            is ConfigUpdate.BrightnessAuto -> {
+                ReadBookConfig.brightnessAuto = update.value
+                _uiState.update {
+                    it.copy(menuConfig = it.menuConfig.copy(brightnessAuto = update.value))
+                }
+                viewModelScope.launch {
+                    readSettingsRepository.setBrightnessAuto(update.value)
+                }
+                _effects.tryEmit(
+                    ReadBookEffect.ToggleBrightnessAuto(
+                        update.value,
+                        _uiState.value.menuConfig.readBrightness
+                    )
+                )
+            }
             is ConfigUpdate.UseUnderlineGlobal -> {
                 ReadConfig.useUnderline = update.value
                 viewModelScope.launch {
@@ -3051,7 +3152,7 @@ class ReadBookViewModel(
                     _effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
                         setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.UpdateContent, ConfigUpdateAction.InvalidateTextPage, ConfigUpdateAction.SubmitRenderTask)
                     ))
-                    if (AppConfig.readBarStyleFollowPage) {
+                    if (ReadConfig.readBarStyleFollowPage) {
                         postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
                     }
                 }
@@ -3061,7 +3162,7 @@ class ReadBookViewModel(
                     _effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
                         setOf(ConfigUpdateAction.UpdateStyle, ConfigUpdateAction.UpdateContent, ConfigUpdateAction.InvalidateTextPage, ConfigUpdateAction.SubmitRenderTask)
                     ))
-                    if (AppConfig.readBarStyleFollowPage) {
+                    if (ReadConfig.readBarStyleFollowPage) {
                         postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
                     }
                 }
@@ -3071,7 +3172,7 @@ class ReadBookViewModel(
                     _effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
                         setOf(ConfigUpdateAction.UpdateBackground)
                     ))
-                    if (AppConfig.readBarStyleFollowPage) {
+                    if (ReadConfig.readBarStyleFollowPage) {
                         postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
                     }
                 }

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

@@ -14,13 +14,15 @@ 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.help.config.AppConfig
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.gone
 import io.legado.app.utils.isAbsUrl
 import io.legado.app.utils.printOnDebug
@@ -28,8 +30,6 @@ import io.legado.app.utils.sendToClip
 import io.legado.app.utils.share
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.visible
-import androidx.core.net.toUri
-import io.legado.app.constant.AppLog
 
 @SuppressLint("RestrictedApi")
 class TextActionMenu(
@@ -185,11 +185,11 @@ class TextActionMenu(
                 callBack.onMenuActionFinally()
             }
             holder.itemView.setOnLongClickListener {
-                if (AppConfig.contentSelectSpeakMod == 0) {
-                    AppConfig.contentSelectSpeakMod = 1
+                if (ReadConfig.contentSelectSpeakMod == 0) {
+                    ReadConfig.contentSelectSpeakMod = 1
                     context.toastOnUi("切换为从选择的地方开始一直朗读")
                 } else {
-                    AppConfig.contentSelectSpeakMod = 0
+                    ReadConfig.contentSelectSpeakMod = 0
                     context.toastOnUi("切换为朗读选择内容")
                 }
                 true

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/page/AutoPager.kt

@@ -4,9 +4,9 @@ import android.graphics.Canvas
 import android.graphics.Paint
 import android.os.SystemClock
 import androidx.core.graphics.withClip
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.page.entities.PageDirection
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeeded
 import io.legado.app.utils.themeColor
@@ -30,7 +30,7 @@ class AutoPager(private val readView: ReadView) : Runnable {
 
     fun start() {
         isRunning = true
-        isEInkMode = AppConfig.isEInkMode
+        isEInkMode = ReadConfig.isEInkMode
         readView.curPage.upSelectAble(false)
         if (isEInkMode) {
             readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
@@ -49,7 +49,7 @@ class AutoPager(private val readView: ReadView) : Runnable {
         isPausing = false
         isEInkMode = false
         readView.removeCallbacks(this)
-        readView.curPage.upSelectAble(AppConfig.textSelectAble)
+        readView.curPage.upSelectAble(ReadConfig.selectText)
         readView.invalidate()
         reset()
         canvasRecorder.recycle()

+ 4 - 4
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -9,7 +9,6 @@ import android.view.View
 import io.legado.app.R
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.book.isOnLineTxt
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.association.OpenUrlConfirmActivity
 import io.legado.app.ui.book.read.page.delegate.PageDelegate
@@ -25,6 +24,7 @@ import io.legado.app.ui.book.read.page.entities.column.TextColumn
 import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.widget.dialog.PhotoDialog
 import io.legado.app.utils.activity
 import io.legado.app.utils.dpToPx
@@ -40,7 +40,7 @@ import kotlin.math.min
  * 阅读内容视图
  */
 class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
-    var selectAble = AppConfig.textSelectAble
+    var selectAble = ReadConfig.selectText
     val selectedPaint by lazy {
         Paint().apply {
             color = context.getCompatColor(R.color.btn_bg_press_2)
@@ -73,7 +73,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     //绘制图片的paint
     val imagePaint by lazy {
         Paint().apply {
-            isAntiAlias = AppConfig.useAntiAlias
+            isAntiAlias = ReadConfig.useAntiAlias
         }
     }
 
@@ -266,7 +266,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                     handled = true
                 }
 
-                is ImageColumn -> when (AppConfig.clickImgWay) {
+                is ImageColumn -> when (ReadConfig.clickImgWay) {
                     "1" -> { //预览图片
                         activity?.showDialogFragment(PhotoDialog(column.src, isBook = true))
                         handled = true

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -16,13 +16,13 @@ import io.legado.app.R
 import io.legado.app.constant.AppConst.timeFormat
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.databinding.ViewBookPageBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.entities.TextLine
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.ui.book.read.page.entities.TextPos
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.widget.BatteryView
 import io.legado.app.utils.activity
 import io.legado.app.utils.applyNavigationBarPadding
@@ -169,7 +169,7 @@ class PageView(
     }
 
     fun upPaddingDisplayCutouts() {
-        if (AppConfig.paddingDisplayCutouts) {
+        if (ReadConfig.paddingDisplayCutouts) {
             binding.vwRoot.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
                 val insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
                 binding.vwRoot.setPadding(

+ 15 - 15
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -13,7 +13,6 @@ import android.widget.FrameLayout
 import io.legado.app.R
 import io.legado.app.constant.PageAnim
 import io.legado.app.data.entities.BookProgress
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
@@ -37,6 +36,7 @@ import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.activity
 import io.legado.app.utils.invisible
 import io.legado.app.utils.longToastOnUi
@@ -272,7 +272,7 @@ class ReadView(
     }
 
     fun cancelSelect(clearSearchResult: Boolean = false) {
-        if (isTextSelected) {
+        if (isTextSelected || clearSearchResult) {
             curPage.cancelSelect(clearSearchResult)
             isTextSelected = false
         }
@@ -398,39 +398,39 @@ class ReadView(
         when {
             isTextSelected -> Unit
             mcRect.contains(startX, startY) -> if (!isAbortAnim) {
-                click(AppConfig.clickActionMC)
+                click(ReadConfig.clickActionMC)
             }
 
             bcRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionBC)
+                click(ReadConfig.clickActionBC)
             }
 
             blRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionBL)
+                click(ReadConfig.clickActionBL)
             }
 
             brRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionBR)
+                click(ReadConfig.clickActionBR)
             }
 
             mlRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionML)
+                click(ReadConfig.clickActionML)
             }
 
             mrRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionMR)
+                click(ReadConfig.clickActionMR)
             }
 
             tlRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionTL)
+                click(ReadConfig.clickActionTL)
             }
 
             tcRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionTC)
+                click(ReadConfig.clickActionTC)
             }
 
             trRect.contains(startX, startY) -> {
-                click(AppConfig.clickActionTR)
+                click(ReadConfig.clickActionTR)
             }
         }
     }
@@ -552,7 +552,7 @@ class ReadView(
                 pageDelegate = NoAnimPageDelegate(this)
             }
         }
-        (pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage
+        (pageDelegate as? ScrollPageDelegate)?.noAnim = ReadConfig.noAnimScrollPage
         if (upRecorder) {
             (pageDelegate as? HorizontalPageDelegate)?.upRecorder()
             autoPager.upRecorder()
@@ -603,7 +603,7 @@ class ReadView(
      * 更新滑动距离
      */
     fun upPageSlopSquare() {
-        val pageTouchSlop = AppConfig.pageTouchSlop
+        val pageTouchSlop = ReadConfig.pageTouchSlop
         this.pageSlopSquare = if (pageTouchSlop == 0) slopSquare else pageTouchSlop
         pageSlopSquare2 = this.pageSlopSquare * this.pageSlopSquare
     }
@@ -697,7 +697,7 @@ class ReadView(
     }
 
     fun invalidateTextPage() {
-        if (!AppConfig.optimizeRender) {
+        if (!ReadConfig.optimizeRender) {
             return
         }
         pageFactory.run {
@@ -722,7 +722,7 @@ class ReadView(
     }
 
     fun submitRenderTask() {
-        if (!AppConfig.optimizeRender) {
+        if (!ReadConfig.optimizeRender) {
             return
         }
         curPage.submitRenderTask()

+ 5 - 6
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt

@@ -9,11 +9,9 @@ import android.graphics.Paint
 import android.graphics.Path
 import android.graphics.Shader
 import android.os.Build
-import android.text.TextPaint
 import androidx.annotation.Keep
 import io.legado.app.help.PaintPool
 import io.legado.app.help.book.isImage
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.ContentTextView
@@ -22,6 +20,7 @@ import io.legado.app.ui.book.read.page.entities.column.BaseColumn
 import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
 import io.legado.app.ui.book.read.page.entities.column.TextColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.dpToPx
@@ -77,7 +76,7 @@ data class TextLine(
     var textPage: TextPage = emptyTextPage
     var isLeftLine = true
     val useUnderline: Boolean
-        get() = AppConfig.useUnderline
+        get() = ReadConfig.useUnderline
 
     fun addColumn(column: BaseColumn) {
         if (column !is TextColumn) {
@@ -160,7 +159,7 @@ data class TextLine(
     }
 
     fun draw(view: ContentTextView, canvas: Canvas) {
-        if (AppConfig.optimizeRender) {
+        if (ReadConfig.optimizeRender) {
             canvasRecorder.recordIfNeededThenDraw(canvas, view.width, height.toInt()) {
                 drawTextLine(view, this)
             }
@@ -236,7 +235,7 @@ data class TextLine(
         val paint = ChapterProvider.contentPaint
         paint.color = ReadBookConfig.durConfig.curUnderlineColor()
         paint.strokeWidth = ReadBookConfig.underlineHeight.toFloat()
-        paint.pathEffect = if (dottedLine && !AppConfig.isEInkMode)
+        paint.pathEffect = if (dottedLine && !ReadConfig.isEInkMode)
             ChapterProvider.dashEffect
         else
             null
@@ -557,7 +556,7 @@ data class TextLine(
     }
 
     fun checkFastDraw(): Boolean {
-        if (!AppConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) {
+        if (!ReadConfig.optimizeRender || exceed || !onlyTextColumn || textPage.isMsgPage) {
             return false
         }
         if (wordSpacing != 0f && (!atLeastApi26 || !wordSpacingWorking)) {

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -9,13 +9,13 @@ import androidx.annotation.Keep
 import androidx.core.graphics.withTranslation
 import io.legado.app.R
 import io.legado.app.help.PaintPool
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.ui.book.read.page.ContentTextView
 import io.legado.app.ui.book.read.page.entities.TextChapter.Companion.emptyTextChapter
 import io.legado.app.ui.book.read.page.entities.column.TextBaseColumn
 import io.legado.app.ui.book.read.page.entities.column.TextColumn
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeeded
 import io.legado.app.utils.dpToPx
@@ -299,7 +299,7 @@ data class TextPage(
     }
 
     fun draw(view: ContentTextView, canvas: Canvas, relativeOffset: Float) {
-        if (AppConfig.optimizeRender) {
+        if (ReadConfig.optimizeRender) {
             render(view)
             canvas.withTranslation(0f, relativeOffset) {
                 canvasRecorder.draw(this)

+ 7 - 7
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -15,12 +15,12 @@ import io.legado.app.constant.EventBus
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.help.book.BookContent
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig.dottedBase
 import io.legado.app.help.config.ReadBookConfig.dottedRatio
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.entities.TextChapter
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.buildMainHandler
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.isContentScheme
@@ -190,7 +190,7 @@ object ChapterProvider {
             displayTitle.splitNotBlank("\n").forEach { text ->
                 setTypeText(
                     book, absStartX, durY,
-                    if (AppConfig.enableReview) text + reviewChar else text,
+                    if (ReadConfig.enableReview) text + reviewChar else text,
                     textPages,
                     stringBuilder,
                     titlePaint,
@@ -279,7 +279,7 @@ object ChapterProvider {
                     if (text.isNotBlank()) {
                         setTypeText(
                             book, absStartX, durY,
-                            if (AppConfig.enableReview) text + reviewChar else text,
+                            if (ReadConfig.enableReview) text + reviewChar else text,
                             textPages,
                             stringBuilder,
                             contentPaint,
@@ -918,7 +918,7 @@ object ChapterProvider {
                 }
 
                 else -> {
-                    when (AppConfig.systemTypefaces) {
+                    when (ReadConfig.systemTypefaces) {
                         1 -> Typeface.SERIF
                         2 -> Typeface.MONOSPACE
                         else -> Typeface.SANS_SERIF
@@ -1013,7 +1013,7 @@ object ChapterProvider {
             tPaint.setFontVariationSettings("'wght' ${ReadBookConfig.titleBold}")
         tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
         tPaint.isAntiAlias = true
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && AppConfig.optimizeRender) {
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && ReadConfig.optimizeRender) {
             tPaint.isLinearText = true
         }
         if (ReadBookConfig.textItalic) {
@@ -1036,7 +1036,7 @@ object ChapterProvider {
             cPaint.setFontVariationSettings("'wght' ${ReadBookConfig.textBold}")
         cPaint.textSize = ReadBookConfig.textSize.toFloat().spToPx()
         cPaint.isAntiAlias = true
-        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && AppConfig.optimizeRender) {
+        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q && ReadConfig.optimizeRender) {
             cPaint.isLinearText = true
         }
         if (ReadBookConfig.textItalic) {
@@ -1086,7 +1086,7 @@ object ChapterProvider {
      * 更新绘制尺寸
      */
     fun upLayout() {
-        when (AppConfig.doublePageHorizontal) {
+        when (ReadConfig.doubleHorizontalPage) {
             "0" -> doublePage = false
             "1" -> doublePage = true
             "2" -> {

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -27,7 +27,6 @@ import io.legado.app.data.repository.HighlightRuleRepository
 import io.legado.app.help.book.BookContent
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.getBookSource
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.ImageProvider
@@ -45,6 +44,7 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
 import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceChar
 import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharC
 import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharD
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.StringUtils
 import io.legado.app.utils.dpToPx
@@ -141,7 +141,7 @@ class TextChapterLayout(
     private val useZhLayout = ReadBookConfig.useZhLayout
     private val isMiddleTitle = ReadBookConfig.isMiddleTitle
     private val textFullJustify = ReadBookConfig.textFullJustify
-    private val adaptSpecialStyle = AppConfig.adaptSpecialStyle
+    private val adaptSpecialStyle = ReadConfig.adaptSpecialStyle
     private val pageAnim = book.getPageAnim()
     private val titleSegType = ReadBookConfig.titleSegType
     private val titleSegDistance = ReadBookConfig.titleSegDistance
@@ -490,7 +490,7 @@ class TextChapterLayout(
                 text = sb.toString()
                 if (text.isNotBlank()) {
                     wordCount += text.replace(noWordCountRegex,"").length
-                    val textToType = if (AppConfig.enableReview) text + reviewChar else text
+                    val textToType = if (ReadConfig.enableReview) text + reviewChar else text
                     setTypeText(
                         book,
                         textToType,

+ 111 - 17
app/src/main/java/io/legado/app/ui/book/read/sheet/EffectiveReplacesSheet.kt

@@ -1,24 +1,42 @@
 package io.legado.app.ui.book.read.sheet
 
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 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.items
-import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.Close
 import androidx.compose.runtime.Composable
 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.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
+import io.legado.app.data.appDb
+import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.model.ReadBook
+import io.legado.app.ui.config.readConfig.ReadConfig
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.series.MediumTonalButton
+import io.legado.app.ui.widget.components.card.NormalCard
+import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.text.AppText
+import kotlinx.coroutines.launch
+
+private const val CHINESE_CONVERT_ID = -1L
 
 @Composable
 fun EffectiveReplacesSheet(
@@ -26,12 +44,26 @@ fun EffectiveReplacesSheet(
     onDismissRequest: () -> Unit,
     onOpenReplaceEditor: (id: Long, pattern: String?) -> Unit,
     onReplaceRuleChanged: () -> Unit,
+    onNavigateToTextEffects: () -> Unit,
 ) {
-    val effectiveRules = remember {
+    val scope = rememberCoroutineScope()
+    val chineseConvertActive = ReadConfig.chineseConverterType > 0
+    val chineseConvertItem = remember { ReplaceRule(CHINESE_CONVERT_ID, "繁简转换") }
+
+    val effectiveRules = remember(show) {
         ReadBook.curTextChapter?.effectiveReplaceRules ?: emptyList()
     }
 
-    var isEdited by remember { mutableStateOf(false) }
+    val items = remember(show, effectiveRules, chineseConvertActive) {
+        if (chineseConvertActive) {
+            effectiveRules + chineseConvertItem
+        } else {
+            effectiveRules
+        }
+    }
+
+    var isEdited by remember(show) { mutableStateOf(false) }
+    var disabledIds by remember(show) { mutableStateOf(emptySet<Long>()) }
 
     AppModalBottomSheet(
         show = show,
@@ -44,18 +76,80 @@ fun EffectiveReplacesSheet(
         LazyColumn(
             modifier = Modifier.fillMaxWidth(),
         ) {
-            items(effectiveRules, key = { it.id }) { rule ->
-                Text(
-                    text = rule.name,
-                    style = MaterialTheme.typography.bodyLarge,
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .clickable {
-                            onOpenReplaceEditor(rule.id, rule.pattern)
+            items(items, key = { it.id }) { rule ->
+                if (rule.id !in disabledIds) {
+                    NormalCard(
+                        onClick = {
+                            if (rule.id == CHINESE_CONVERT_ID) {
+                                onNavigateToTextEffects()
+                            } else {
+                                onOpenReplaceEditor(rule.id, rule.pattern)
+                            }
+                        },
+                        modifier = Modifier
+                            .fillMaxWidth()
+                            .padding(vertical = 4.dp),
+                        containerColor = LegadoTheme.colorScheme.onSheetContent
+                    ) {
+                        Row(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .padding(horizontal = 12.dp, vertical = 8.dp),
+                            verticalAlignment = Alignment.CenterVertically,
+                        ) {
+                            Column(modifier = Modifier.weight(1f)) {
+                                AppText(
+                                    text = rule.name,
+                                    style = LegadoTheme.typography.labelLargeEmphasized,
+                                    maxLines = 1,
+                                    overflow = TextOverflow.Ellipsis,
+                                )
+                                if (rule.id != CHINESE_CONVERT_ID) {
+                                    Row(verticalAlignment = Alignment.CenterVertically) {
+                                        AppText(
+                                            text = rule.pattern,
+                                            style = LegadoTheme.typography.bodySmall,
+                                            color = LegadoTheme.colorScheme.onSurfaceVariant,
+                                            maxLines = 1,
+                                            overflow = TextOverflow.Ellipsis,
+                                        )
+                                        Spacer(Modifier.width(4.dp))
+                                        AppIcon(
+                                            imageVector = Icons.AutoMirrored.Filled.ArrowForward,
+                                            contentDescription = null,
+                                            modifier = Modifier.size(14.dp),
+                                            tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                                        )
+                                        Spacer(Modifier.width(4.dp))
+                                        AppText(
+                                            text = rule.replacement.ifEmpty { "∅" },
+                                            style = LegadoTheme.typography.bodySmall,
+                                            color = LegadoTheme.colorScheme.onSurfaceVariant,
+                                            maxLines = 1,
+                                            overflow = TextOverflow.Ellipsis,
+                                            modifier = Modifier.weight(1f),
+                                        )
+                                    }
+                                }
+                            }
+                            MediumTonalButton(
+                                onClick = {
+                                    disabledIds = disabledIds + rule.id
+                                    isEdited = true
+                                    if (rule.id == CHINESE_CONVERT_ID) {
+                                        ReadConfig.chineseConverterType = 0
+                                    } else {
+                                        scope.launch {
+                                            rule.isEnabled = false
+                                            appDb.replaceRuleDao.insert(rule)
+                                        }
+                                    }
+                                },
+                                icon = Icons.Default.Close
+                            )
                         }
-                        .padding(vertical = 12.dp),
-                )
-                HorizontalDivider()
+                    }
+                }
             }
         }
     }

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

@@ -87,9 +87,6 @@ fun MoreConfigSheet(
                 onUseZhLayoutChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.UseZhLayout(it)))
                 },
-                onShowBrightnessViewChange = {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowBrightnessView(it)))
-                },
                 onUseUnderlineChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.UseUnderlineGlobal(it)))
                 },
@@ -170,7 +167,6 @@ private fun ScreenSettings(
     onTextBottomJustifyChange: (Boolean) -> Unit,
     onAdaptSpecialStyleChange: (Boolean) -> Unit,
     onUseZhLayoutChange: (Boolean) -> Unit,
-    onShowBrightnessViewChange: (Boolean) -> Unit,
     onUseUnderlineChange: (Boolean) -> Unit,
 ) {
     val screenDirectionEntries = stringArrayResource(R.array.screen_direction_title)
@@ -232,11 +228,6 @@ private fun ScreenSettings(
         checked = preferences.useZhLayout,
         onCheckedChange = onUseZhLayoutChange,
     )
-    TinySwitchSettingItem(
-        title = stringResource(R.string.show_brightness_view),
-        checked = preferences.showBrightnessView,
-        onCheckedChange = onShowBrightnessViewChange,
-    )
     TinySwitchSettingItem(
         title = stringResource(R.string.use_underline),
         checked = preferences.useUnderline,

+ 2 - 54
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt

@@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
@@ -23,62 +22,9 @@ import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookStyleConfig
-import io.legado.app.ui.widget.components.SectionTitle
-import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import kotlinx.coroutines.launch
 
-@Composable
-fun ReadStyleSheet(
-    onDismissRequest: () -> Unit,
-    onOpenPaddingConfig: () -> Unit,
-    onOpenMoreConfig: () -> Unit,
-    onOpenBgTextConfig: (Int) -> Unit,
-    onOpenShadowSet: () -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
-    onOpenHighlightRule: () -> Unit,
-    onOpenFontSelect: () -> Unit,
-    onToggleDayNight: () -> Unit,
-    readMenuCustomIcons: Map<String, String> = emptyMap(),
-    bottomBarButtons: List<ReadBookButtonConfigItem> = emptyList(),
-    onIntent: (ReadBookIntent) -> Unit,
-    styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
-) {
-    var showTextTitle by remember { mutableStateOf(false) }
-
-    AppModalBottomSheet(
-        show = true,
-        onDismissRequest = {
-            onIntent(ReadBookIntent.SaveReadStyleConfig)
-            onDismissRequest()
-        },
-        title = stringResource(R.string.read_config),
-    ) {
-        ReadStyleContent(
-            onOpenPaddingConfig = onOpenPaddingConfig,
-            onOpenMoreConfig = onOpenMoreConfig,
-            onOpenBgTextConfig = onOpenBgTextConfig,
-            onOpenTextTitle = { showTextTitle = true },
-            onOpenFontSelect = onOpenFontSelect,
-            onToggleDayNight = onToggleDayNight,
-            readMenuCustomIcons = readMenuCustomIcons,
-            bottomBarButtons = bottomBarButtons,
-            onIntent = onIntent,
-            styleConfig = styleConfig,
-        )
-
-        TextTitlePage(
-            show = showTextTitle,
-            onDismissRequest = { showTextTitle = false },
-            onOpenShadowSet = onOpenShadowSet,
-            onOpenUnderlineConfig = onOpenUnderlineConfig,
-            onOpenHighlightRule = onOpenHighlightRule,
-            onOpenFontSelect = onOpenFontSelect,
-            onIntent = onIntent,
-        )
-    }
-}
-
 @Composable
 fun ReadStyleContent(
     onOpenPaddingConfig: () -> Unit,
@@ -87,6 +33,7 @@ fun ReadStyleContent(
     onOpenTextTitle: () -> Unit,
     onOpenFontSelect: () -> Unit,
     onToggleDayNight: () -> Unit,
+    onPageChanged: (Int) -> Unit = {},
     readMenuCustomIcons: Map<String, String> = emptyMap(),
     bottomBarButtons: List<ReadBookButtonConfigItem> = emptyList(),
     modifier: Modifier = Modifier,
@@ -100,6 +47,7 @@ fun ReadStyleContent(
     LaunchedEffect(pagerState) {
         snapshotFlow { pagerState.currentPage }.collect { page ->
             currentPage = page
+            onPageChanged(page)
         }
     }
 

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

@@ -340,6 +340,31 @@ private fun GlobalMenuTab(
 
         Spacer(Modifier.height(8.dp))
 
+        SectionTitle(stringResource(R.string.show_brightness_view))
+
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.show_brightness_view),
+            selectedValue = preferences.showBrightnessView,
+            displayEntries = stringArrayResource(R.array.brightness_bar_mode_title),
+            entryValues = stringArrayResource(R.array.brightness_bar_mode_value),
+            onValueChange = {
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowBrightnessView(it)))
+            },
+        )
+        if (preferences.showBrightnessView == "2") {
+            TinyDropdownSettingItem(
+                title = stringResource(R.string.brightness_bar_position),
+                selectedValue = preferences.brightnessVwPos,
+                displayEntries = stringArrayResource(R.array.brightness_bar_position_title),
+                entryValues = stringArrayResource(R.array.brightness_bar_position_value),
+                onValueChange = {
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.BrightnessVwPos(it)))
+                },
+            )
+        }
+
+        Spacer(Modifier.height(8.dp))
+
         var borderEnabled by remember {
             mutableStateOf(preferences.readMenuBorderWidth > 0)
         }

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

@@ -41,7 +41,6 @@ import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
-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.TinyColorSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
@@ -55,51 +54,6 @@ private const val COLOR_TEXT = 1
 private const val COLOR_ACCENT = 2
 private const val COLOR_TITLE = 4
 
-// ========== Text & Title Sheet ==========
-
-@Composable
-internal fun TextTitlePage(
-    show: Boolean,
-    onDismissRequest: () -> Unit,
-    onOpenShadowSet: () -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
-    onOpenHighlightRule: () -> Unit,
-    onOpenFontSelect: () -> Unit,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    val scope = rememberCoroutineScope()
-    val tabTitles = listOf(
-        stringResource(R.string.read_config_text_effects),
-        stringResource(R.string.read_config_layout_spacing),
-        stringResource(R.string.read_config_title_settings),
-    )
-    val pagerState = rememberPagerState(pageCount = { 3 })
-    var selectedTab by remember { mutableIntStateOf(0) }
-
-    LaunchedEffect(pagerState) {
-        snapshotFlow { pagerState.settledPage }.collect { selectedTab = it }
-    }
-
-    AppModalBottomSheet(
-        show = show,
-        onDismissRequest = onDismissRequest,
-        title = stringResource(R.string.read_config_text_effects),
-    ) {
-        ReadStyleTextTitleContent(
-            tabTitles = tabTitles,
-            selectedTab = selectedTab,
-            onSelectedTabChange = { selectedTab = it },
-            pagerState = pagerState,
-            onOpenShadowSet = onOpenShadowSet,
-            onOpenUnderlineConfig = onOpenUnderlineConfig,
-            onOpenHighlightRule = onOpenHighlightRule,
-            onOpenFontSelect = onOpenFontSelect,
-            animateToPage = { page -> scope.launch { pagerState.animateScrollToPage(page) } },
-            onIntent = onIntent,
-        )
-    }
-}
-
 @Composable
 fun ReadStyleTextTitleContent(
     onOpenShadowSet: () -> Unit,

+ 0 - 17
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentActivity.kt

@@ -1,17 +0,0 @@
-package io.legado.app.ui.book.searchContent
-
-import androidx.compose.runtime.Composable
-import io.legado.app.base.BaseComposeActivity
-import io.legado.app.ui.theme.AppTheme
-
-class SearchContentActivity : BaseComposeActivity() {
-    @Composable
-    override fun Content() {
-
-        AppTheme {
-            SearchContentScreen(
-                onBack = { finish() }
-            )
-        }
-    }
-}

+ 31 - 0
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt

@@ -0,0 +1,31 @@
+package io.legado.app.ui.book.searchContent
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+/**
+ * Singleton bus for passing search results from [SearchContentScreen] back to the reader.
+ *
+ * Uses [MutableSharedFlow] with `replay = 1` so the reader entry receives the result
+ * even if it subscribes after the emission (the reader entry is not composed while
+ * the search route is on top of the NavDisplay back stack).
+ */
+object SearchContentResult {
+
+    data class Result(
+        val searchResults: List<SearchResult>,
+        val index: Int,
+        val query: String,
+    )
+
+    private val _results = MutableSharedFlow<Result>(replay = 1)
+    val results = _results.asSharedFlow()
+
+    fun emitResult(result: Result) {
+        _results.tryEmit(result)
+    }
+
+    fun resetReplayCache() {
+        _results.resetReplayCache()
+    }
+}

+ 2 - 12
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt

@@ -1,8 +1,5 @@
 package io.legado.app.ui.book.searchContent
 
-import android.app.Activity
-import android.content.Intent
-import androidx.activity.compose.LocalActivity
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.clickable
@@ -81,8 +78,6 @@ fun SearchContentScreen(
     onBack: () -> Unit,
     viewModel: SearchContentViewModel = koinViewModel()
 ) {
-    val activity = LocalActivity.current
-
     val uiState by viewModel.uiState.collectAsState()
     val searchQuery by viewModel.searchQuery.collectAsState()
     val replaceEnabled by viewModel.replaceEnabled.collectAsState()
@@ -259,13 +254,8 @@ fun SearchContentScreen(
                                     result = result,
                                     isCurrentChapter = result.chapterIndex == durChapterIndex,
                                     onClick = {
-                                        viewModel.onSearchResultClick(result) { key ->
-                                            val intent = Intent().apply {
-                                                putExtra("key", key)
-                                                putExtra("index", index)
-                                            }
-                                            activity?.setResult(Activity.RESULT_OK, intent)
-                                            activity?.finish()
+                                        if (viewModel.onSearchResultClick(result)) {
+                                            onBack()
                                         }
                                     }
                                 )

+ 15 - 15
app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt

@@ -1,16 +1,12 @@
 package io.legado.app.ui.book.searchContent
 
-import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import io.legado.app.constant.EventBus
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.SearchContentHistory
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.SearchContentRepository
-import io.legado.app.help.IntentData
-import io.legado.app.utils.postEvent
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -42,15 +38,13 @@ sealed interface SearchContentState {
 
 
 class SearchContentViewModel(
-    savedStateHandle: SavedStateHandle,
+    val bookUrl: String,
+    initialSearchWord: String?,
+    private val searchResultIndex: Int,
     private val bookRepository: BookRepository,
     private val searchContentRepository: SearchContentRepository
 ) : ViewModel() {
 
-    val bookUrl: String = savedStateHandle.get<String>("bookUrl") ?: ""
-    private val initialSearchWord: String? = savedStateHandle.get<String>("searchWord")
-    private val searchResultIndex: Int = savedStateHandle.get<Int>("searchResultIndex") ?: 0
-
     private val _searchQuery = MutableStateFlow(initialSearchWord ?: "")
     val searchQuery = _searchQuery.asStateFlow()
 
@@ -189,12 +183,18 @@ class SearchContentViewModel(
         hasAutoScrolled = true
     }
 
-    fun onSearchResultClick(searchResult: SearchResult, onSuccess: (key: Long) -> Unit) {
+    fun onSearchResultClick(searchResult: SearchResult): Boolean {
         stopSearch()
-        postEvent(EventBus.SEARCH_RESULT, uiState.value.searchResults)
-        val key = System.currentTimeMillis()
-        IntentData.put("searchResult$key", searchResult)
-        IntentData.put("searchResultList$key", uiState.value.searchResults)
-        onSuccess(key)
+        val results = uiState.value.searchResults
+        val index = results.indexOf(searchResult)
+        if (index < 0) return false
+        SearchContentResult.emitResult(
+            SearchContentResult.Result(
+                searchResults = results,
+                index = index,
+                query = searchResult.query
+            )
+        )
+        return true
     }
 }

+ 212 - 1
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfig.kt

@@ -1,10 +1,139 @@
 package io.legado.app.ui.config.readConfig
 
+import io.legado.app.BuildConfig
 import io.legado.app.constant.PreferKey
+import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.ui.config.prefDelegate
+import io.legado.app.ui.config.themeConfig.ThemeConfig
+import io.legado.app.utils.isNightMode
+import io.legado.app.utils.sysConfiguration
 
 object ReadConfig {
 
+    val isEInkMode: Boolean
+        get() = ThemeConfig.appTheme == "4"
+
+    var isNightTheme: Boolean
+        get() = when (ThemeConfig.themeMode) {
+            "1" -> false
+            "2" -> true
+            else -> sysConfiguration.isNightMode
+        }
+        set(value) {
+            val newMode = if (value) "2" else "1"
+            if (ThemeConfig.themeMode != newMode) {
+                ThemeConfig.themeMode = newMode
+            }
+        }
+
+    var useAntiAlias by prefDelegate(
+        PreferKey.antiAlias,
+        false
+    )
+
+    val enableReview: Boolean
+        get() = BuildConfig.DEBUG && enableReviewPref
+
+    private var enableReviewPref by prefDelegate(
+        PreferKey.enableReview,
+        false
+    )
+
+    var systemTypefaces by prefDelegate(
+        PreferKey.systemTypefaces,
+        0
+    )
+
+    var readUrlInBrowser by prefDelegate(
+        PreferKey.readUrlOpenInBrowser,
+        false
+    )
+
+    var contentSelectSpeakMod by prefDelegate(
+        PreferKey.contentSelectSpeakMod,
+        0
+    )
+
+    var syncBookProgress by prefDelegate(
+        PreferKey.syncBookProgress,
+        true
+    )
+
+    var syncBookProgressPlus by prefDelegate(
+        PreferKey.syncBookProgressPlus,
+        false
+    )
+
+    var preDownloadNum by prefDelegate(
+        PreferKey.preDownloadNum,
+        10
+    )
+
+    var imageRetainNum by prefDelegate(
+        PreferKey.imageRetainNum,
+        0
+    )
+
+    var audioCacheCleanTimeOrgin by prefDelegate(
+        PreferKey.audioCacheCleanTime,
+        10
+    )
+
+    val audioCacheCleanTime: Long
+        get() = audioCacheCleanTimeOrgin * 60 * 1000L
+
+    var audioPreDownloadNum by prefDelegate(
+        PreferKey.audioPreDownloadNum,
+        10
+    )
+
+    private const val defaultSpeechRate = 5
+
+    val speechRatePlay: Int
+        get() = if (ttsFollowSys) defaultSpeechRate else ttsSpeechRate
+
+    fun syncReadPreferences(preferences: ReadPreferences) {
+        optimizeRender = preferences.optimizeRender
+        adaptSpecialStyle = preferences.adaptSpecialStyle
+        useUnderline = preferences.useUnderline
+        brightnessVwPos = preferences.brightnessVwPos
+        readBrightness = preferences.readBrightness
+        brightnessAuto = preferences.brightnessAuto
+        chineseConverterType = preferences.chineseConverterType
+        clickActionTL = preferences.clickActionTL
+        clickActionTC = preferences.clickActionTC
+        clickActionTR = preferences.clickActionTR
+        clickActionML = preferences.clickActionML
+        clickActionMC = preferences.clickActionMC
+        clickActionMR = preferences.clickActionMR
+        clickActionBL = preferences.clickActionBL
+        clickActionBC = preferences.clickActionBC
+        clickActionBR = preferences.clickActionBR
+        screenOrientation = preferences.screenOrientation
+        noAnimScrollPage = preferences.noAnimScrollPage
+        tocUiUseReplace = preferences.tocUiUseReplace
+        tocCountWords = preferences.tocCountWords
+        autoChangeSource = preferences.autoChangeSource
+        clickImgWay = preferences.clickImgWay
+        doubleHorizontalPage = preferences.doubleHorizontalPage
+        progressBarBehavior = preferences.progressBarBehavior
+        keyPageOnLongPress = preferences.keyPageOnLongPress
+        volumeKeyPage = preferences.volumeKeyPage
+        volumeKeyPageOnPlay = preferences.volumeKeyPageOnPlay
+        mouseWheelPage = preferences.mouseWheelPage
+        paddingDisplayCutouts = preferences.paddingDisplayCutouts
+        pageTouchSlop = preferences.pageTouchSlop
+        showReadTitleAddition = preferences.showReadTitleAddition
+        titleBarMode = preferences.titleBarMode
+        menuAlpha = preferences.menuAlpha
+        readSliderMode = preferences.readSliderMode
+        readBarStyleFollowPage = preferences.readBarStyleFollowPage
+        readBarStyle = preferences.readBarStyle
+        defaultSourceChangeAll = preferences.defaultSourceChangeAll
+        sliderVibrator = preferences.sliderVibrator
+        selectVibrator = preferences.selectVibrator
+    }
+
     var tocUiUseReplace by prefDelegate(
         PreferKey.tocUiUseReplace,
         false
@@ -82,7 +211,22 @@ object ReadConfig {
 
     var showBrightnessView by prefDelegate(
         PreferKey.showBrightnessView,
-        true
+        "1"
+    )
+
+    var brightnessVwPos by prefDelegate(
+        PreferKey.brightnessVwPos,
+        "1"
+    )
+
+    var readBrightness by prefDelegate(
+        PreferKey.brightness,
+        100
+    )
+
+    var brightnessAuto by prefDelegate(
+        PreferKey.brightnessAuto,
+        false
     )
 
     var useUnderline by prefDelegate(
@@ -180,6 +324,73 @@ object ReadConfig {
         true
     )
 
+    var clickActionTL by prefDelegate(
+        PreferKey.clickActionTL,
+        2
+    )
+
+    var clickActionTC by prefDelegate(
+        PreferKey.clickActionTC,
+        2
+    )
+
+    var clickActionTR by prefDelegate(
+        PreferKey.clickActionTR,
+        1
+    )
+
+    var clickActionML by prefDelegate(
+        PreferKey.clickActionML,
+        2
+    )
+
+    var clickActionMC by prefDelegate(
+        PreferKey.clickActionMC,
+        0
+    )
+
+    var clickActionMR by prefDelegate(
+        PreferKey.clickActionMR,
+        1
+    )
+
+    var clickActionBL by prefDelegate(
+        PreferKey.clickActionBL,
+        2
+    )
+
+    var clickActionBC by prefDelegate(
+        PreferKey.clickActionBC,
+        1
+    )
+
+    var clickActionBR by prefDelegate(
+        PreferKey.clickActionBR,
+        1
+    )
+
+    fun hasMenuClickArea(): Boolean {
+        return clickActionTL * clickActionTC * clickActionTR *
+                clickActionML * clickActionMC * clickActionMR *
+                clickActionBL * clickActionBC * clickActionBR == 0
+    }
+
+    fun detectClickArea() {
+        if (!hasMenuClickArea()) {
+            clickActionMC = 0
+        }
+    }
+
+    var readBarStyleFollowPage by prefDelegate(
+        PreferKey.readBarStyleFollowPage,
+        false
+    )
+
+    var readBarStyle by prefDelegate(
+        PreferKey.readBarStyle,
+        0
+    )
+
     var prevKeys by prefDelegate(
         PreferKey.prevKeys,
         ""

+ 5 - 2
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigContract.kt

@@ -14,7 +14,9 @@ data class ReadConfigUiState(
     val textBottomJustify: Boolean = true,
     val adaptSpecialStyle: Boolean = true,
     val useZhLayout: Boolean = false,
-    val showBrightnessView: Boolean = true,
+    val showBrightnessView: String = "1",
+    val brightnessVwPos: String = "1",
+    val brightnessAuto: Boolean = false,
     val useUnderline: Boolean = false,
     val readSliderMode: String = "0",
     val doubleHorizontalPage: String = "0",
@@ -53,7 +55,8 @@ sealed interface ReadConfigIntent {
     data class TextBottomJustifyChanged(val value: Boolean) : ReadConfigIntent
     data class AdaptSpecialStyleChanged(val value: Boolean) : ReadConfigIntent
     data class UseZhLayoutChanged(val value: Boolean) : ReadConfigIntent
-    data class ShowBrightnessViewChanged(val value: Boolean) : ReadConfigIntent
+    data class ShowBrightnessViewChanged(val value: String) : ReadConfigIntent
+    data class BrightnessVwPosChanged(val value: String) : ReadConfigIntent
     data class UseUnderlineChanged(val value: Boolean) : ReadConfigIntent
     data class ReadSliderModeChanged(val value: String) : ReadConfigIntent
     data class DoubleHorizontalPageChanged(val value: String) : ReadConfigIntent

+ 18 - 4
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigScreen.kt

@@ -18,13 +18,13 @@ import io.legado.app.R
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SplicedColumnGroup
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
 import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import org.koin.androidx.compose.koinViewModel
 
@@ -173,14 +173,28 @@ fun ReadConfigScreen(
                     }
                 )
 
-                SwitchSettingItem(
+                    DropdownListSettingItem(
                     title = stringResource(R.string.show_brightness_view),
-                    checked = state.showBrightnessView,
-                    onCheckedChange = {
+                        selectedValue = state.showBrightnessView,
+                        displayEntries = stringArrayResource(R.array.brightness_bar_mode_title),
+                        entryValues = stringArrayResource(R.array.brightness_bar_mode_value),
+                        onValueChange = {
                         viewModel.onIntent(ReadConfigIntent.ShowBrightnessViewChanged(it))
                     }
                 )
 
+                    if (state.showBrightnessView == "2") {
+                        DropdownListSettingItem(
+                            title = stringResource(R.string.brightness_bar_position),
+                            selectedValue = state.brightnessVwPos,
+                            displayEntries = stringArrayResource(R.array.brightness_bar_position_title),
+                            entryValues = stringArrayResource(R.array.brightness_bar_position_value),
+                            onValueChange = {
+                                viewModel.onIntent(ReadConfigIntent.BrightnessVwPosChanged(it))
+                            }
+                        )
+                    }
+
                 SwitchSettingItem(
                     title = stringResource(R.string.use_underline),
                     checked = state.useUnderline,

+ 6 - 0
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigViewModel.kt

@@ -92,6 +92,10 @@ class ReadConfigViewModel(
                     readSettingsRepository.setShowBrightnessView(intent.value)
                 }
 
+                is ReadConfigIntent.BrightnessVwPosChanged -> {
+                    readSettingsRepository.setBrightnessVwPos(intent.value)
+                }
+
                 is ReadConfigIntent.UseUnderlineChanged -> {
                     readSettingsRepository.setUseUnderline(intent.value)
                 }
@@ -209,6 +213,8 @@ class ReadConfigViewModel(
             adaptSpecialStyle = adaptSpecialStyle,
             useZhLayout = useZhLayout,
             showBrightnessView = showBrightnessView,
+            brightnessVwPos = brightnessVwPos,
+            brightnessAuto = brightnessAuto,
             useUnderline = useUnderline,
             readSliderMode = readSliderMode,
             doubleHorizontalPage = doubleHorizontalPage,

+ 23 - 1
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -41,17 +41,19 @@ import io.legado.app.ui.book.readRecord.ReadRecordScreen
 import io.legado.app.ui.book.search.SearchIntent
 import io.legado.app.ui.book.search.SearchScreen
 import io.legado.app.ui.book.search.SearchViewModel
+import io.legado.app.ui.book.searchContent.SearchContentScreen
+import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.config.ConfigNavScreen
 import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.coverConfig.CoverConfigScreen
 import io.legado.app.ui.config.customTheme.CustomThemeScreen
 import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigScreen
+import io.legado.app.ui.config.labConfig.LabConfigScreen
 import io.legado.app.ui.config.otherConfig.OtherConfigScreen
 import io.legado.app.ui.config.readConfig.ReadConfigScreen
 import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
 import io.legado.app.ui.config.themeManage.ThemeManageScreen
-import io.legado.app.ui.config.labConfig.LabConfigScreen
 import io.legado.app.ui.config.translation.TranslationConfigScreen
 import io.legado.app.ui.rss.article.MainRouteRssSort
 import io.legado.app.ui.rss.article.RssSortRouteScreen
@@ -69,6 +71,7 @@ import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.koin.androidx.compose.koinViewModel
+import org.koin.core.parameter.parametersOf
 
 @OptIn(ExperimentalSharedTransitionApi::class)
 fun MainActivity.mainEntryProvider(
@@ -294,6 +297,15 @@ fun MainActivity.mainEntryProvider(
             host = controller,
             controller = controller,
             onEffectsReady = { effectsReady.complete(Unit) },
+            onOpenSearch = { word, bookUrl ->
+                onNavigateToRoute(
+                    MainRouteSearchContent(
+                        bookUrl = bookUrl,
+                        searchWord = word,
+                        searchResultIndex = readBookViewModel.uiState.value.searchResultIndex
+                    )
+                )
+            },
         )
 
         DisposableEffect(controller, lifecycleOwner, route.readAloud) {
@@ -340,6 +352,16 @@ fun MainActivity.mainEntryProvider(
         }
     }
 
+    entry<MainRouteSearchContent> { route ->
+        val viewModel = koinViewModel<SearchContentViewModel>(
+            parameters = { parametersOf(route) }
+        )
+        SearchContentScreen(
+            viewModel = viewModel,
+            onBack = { onNavigateBack() },
+        )
+    }
+
     entry<MainRouteSearch> { route ->
         val searchViewModel = koinViewModel<SearchViewModel>()
 

+ 8 - 0
app/src/main/java/io/legado/app/ui/main/MainNavKey.kt

@@ -97,6 +97,13 @@ data class MainRouteExploreShow(
     val exploreUrl: String?,
 ) : MainRoute
 
+@Serializable
+data class MainRouteSearchContent(
+    val bookUrl: String,
+    val searchWord: String? = null,
+    val searchResultIndex: Int = 0,
+) : MainRoute
+
 @Serializable
 data object MainRouteAbout : MainRoute
 
@@ -118,6 +125,7 @@ object MainRouteConst {
     const val ROUTE_BOOK_CACHE_MANAGE = "book/cache/manage"
     const val ROUTE_READ_BOOK = "book/read"
     const val ROUTE_SEARCH = "search"
+    const val ROUTE_SEARCH_CONTENT = "book/searchContent"
     const val ROUTE_BOOK_INFO = "book/info"
     const val ROUTE_EXPLORE_SHOW = "explore/show"
     const val ROUTE_RSS_SORT = "rss/sort"

+ 4 - 0
app/src/main/java/io/legado/app/ui/main/MainNavigator.kt

@@ -60,6 +60,10 @@ object MainNavigator {
                 }
             }
 
+            is MainRouteSearchContent -> {
+                backStack.add(route)
+            }
+
             is MainRouteSearch -> {
                 if (
                     currentRoute == MainRouteHome ||

+ 3 - 3
app/src/main/java/io/legado/app/ui/widget/BatteryViewOrgin.kt

@@ -11,7 +11,7 @@ import android.text.StaticLayout
 import android.util.AttributeSet
 import androidx.annotation.ColorInt
 import androidx.appcompat.widget.AppCompatTextView
-import io.legado.app.help.config.AppConfig
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 import io.legado.app.utils.canvasrecorder.recordIfNeededThenDraw
 import io.legado.app.utils.dpToPx
@@ -75,7 +75,7 @@ class BatteryViewOrgin @JvmOverloads constructor(
     }
 
     override fun onDraw(canvas: Canvas) {
-        if (AppConfig.optimizeRender) {
+        if (ReadConfig.optimizeRender) {
             canvasRecorder.recordIfNeededThenDraw(canvas, width, height) {
                 super.onDraw(this)
                 drawBattery(this)
@@ -119,4 +119,4 @@ class BatteryViewOrgin @JvmOverloads constructor(
         canvasRecorder?.invalidate()
     }
 
-}
+}

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

@@ -0,0 +1,68 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredHeight
+import androidx.compose.foundation.layout.requiredWidth
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import io.legado.app.ui.theme.LegadoTheme.composeEngine
+import io.legado.app.ui.theme.ThemeResolver
+import androidx.compose.material3.Slider as M3Slider
+import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
+
+/**
+ * Vertical slider component that supports both Miuix and M3 engines.
+ *
+ * @param height Visual height of the vertical slider.
+ */
+@Composable
+fun AppVerticalSlider(
+    value: Float,
+    onValueChange: (Float) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    steps: Int = 0,
+    onValueChangeFinished: (() -> Unit)? = null,
+    height: Dp = 160.dp,
+) {
+    val sliderModifier = Modifier
+        .requiredWidth(height)
+        .requiredHeight(48.dp)
+        .graphicsLayer { rotationZ = -90f }
+
+    Box(
+        modifier = modifier
+            .width(48.dp)
+            .height(height),
+        contentAlignment = Alignment.Center,
+    ) {
+        if (ThemeResolver.isMiuixEngine(composeEngine)) {
+            MiuixSlider(
+                value = value,
+                onValueChange = onValueChange,
+                modifier = sliderModifier,
+                enabled = enabled,
+                valueRange = valueRange,
+                steps = steps,
+                onValueChangeFinished = onValueChangeFinished,
+            )
+        } else {
+            M3Slider(
+                value = value,
+                onValueChange = onValueChange,
+                modifier = sliderModifier,
+                enabled = enabled,
+                valueRange = valueRange,
+                steps = steps,
+                onValueChangeFinished = onValueChangeFinished,
+            )
+        }
+    }
+}

+ 3 - 3
app/src/main/java/io/legado/app/ui/widget/components/FontSelectGrid.kt

@@ -143,14 +143,14 @@ private fun FontItem(
         modifier = Modifier
             .fillMaxWidth()
             .clip(RoundedCornerShape(8.dp))
-            .background(LegadoTheme.colorScheme.surfaceVariant)
+            .background(LegadoTheme.colorScheme.surface)
             .clickable(onClick = onClick)
             .padding(12.dp),
     ) {
         Row(
             verticalAlignment = Alignment.CenterVertically,
+            modifier = Modifier.height(48.dp)
         ) {
-            val context = LocalContext.current
             AndroidView(
                 factory = { ctx ->
                     android.widget.TextView(ctx).apply {
@@ -178,7 +178,7 @@ private fun FontItem(
                 Icon(
                     Icons.Default.Check,
                     contentDescription = null,
-                    tint = MaterialTheme.colorScheme.primary,
+                    tint = LegadoTheme.colorScheme.primary,
                     modifier = Modifier.size(18.dp),
                 )
             }

+ 2 - 2
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt

@@ -1,7 +1,7 @@
 package io.legado.app.utils.canvasrecorder
 
 import android.os.Build
-import io.legado.app.help.config.AppConfig
+import io.legado.app.ui.config.readConfig.ReadConfig
 
 object CanvasRecorderFactory {
 
@@ -12,7 +12,7 @@ object CanvasRecorderFactory {
     // issue 3868
     fun create(locked: Boolean = false): CanvasRecorder {
         val impl = when {
-            !AppConfig.optimizeRender -> CanvasRecorderImpl()
+            !ReadConfig.optimizeRender -> CanvasRecorderImpl()
             atLeastApi29 -> CanvasRecorderApi29Impl()
             atLeastApi24 -> CanvasRecorderApi23Impl()
             else -> CanvasRecorderImpl()

+ 0 - 93
app/src/main/res/layout/activity_search_content.xml

@@ -1,93 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent">
-
-    <io.legado.app.ui.widget.TitleBar
-        android:id="@+id/title_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:contentLayout="@layout/view_search"
-        app:contentInsetRight="24dp"
-        app:layout_constraintTop_toTopOf="parent" />
-
-    <com.google.android.material.progressindicator.LinearProgressIndicator
-        android:id="@+id/refresh_progress_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:visibility="gone"
-        android:indeterminate="true"
-        app:layout_constraintTop_toBottomOf="@id/title_bar" />
-
-    <io.legado.app.ui.widget.recycler.scroller.FastScrollRecyclerView
-        android:id="@+id/recycler_view"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:overScrollMode="never"
-        app:layout_constraintBottom_toTopOf="@+id/ll_search_base_info"
-        app:layout_constraintTop_toBottomOf="@id/refresh_progress_bar"
-        tools:ignore="SpeakableTextPresentCheck" />
-
-    <LinearLayout
-        android:id="@+id/ll_search_base_info"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:background="?attr/colorSurfaceContainer"
-        android:paddingHorizontal="16dp"
-        android:paddingTop="8dp"
-        android:gravity="center_vertical"
-        android:orientation="horizontal"
-        app:layout_constraintBottom_toBottomOf="parent">
-
-        <TextView
-            android:id="@+id/tv_current_search_info"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_weight="1"
-            android:gravity="center_vertical"
-            android:singleLine="true"
-            android:textSize="14sp"
-            android:textStyle="bold" />
-
-        <Space
-            android:layout_width="20dp"
-            android:layout_height="1dp" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/iv_search_content_top"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-            android:contentDescription="@string/go_to_top"
-            app:icon="@drawable/ic_arrow_drop_up"
-            app:iconGravity="textStart"
-            android:tooltipText="@string/go_to_top"
-            tools:ignore="UnusedAttribute" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/iv_search_content_bottom"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
-            android:contentDescription="@string/go_to_bottom"
-            app:icon="@drawable/ic_arrow_drop_down"
-            app:iconGravity="textStart"
-            android:tooltipText="@string/go_to_bottom"
-            tools:ignore="UnusedAttribute" />
-    </LinearLayout>
-
-    <com.google.android.material.floatingactionbutton.FloatingActionButton
-        android:id="@+id/fb_stop"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_margin="16dp"
-        android:src="@drawable/ic_stop_black_24dp"
-        android:visibility="invisible"
-        android:contentDescription="@string/stop"
-        app:fabSize="mini"
-        app:layout_constraintBottom_toTopOf="@+id/ll_search_base_info"
-        app:layout_constraintRight_toRightOf="parent" />
-
-</androidx.constraintlayout.widget.ConstraintLayout>

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

@@ -907,6 +907,13 @@
     <string name="wrong_format">格式不对</string>
     <string name="error">错误</string>
     <string name="show_brightness_view">显示亮度调节控件</string>
+    <string name="brightness_bar_off">关闭</string>
+    <string name="brightness_bar_horizontal">横排</string>
+    <string name="brightness_bar_vertical">竖排</string>
+    <string name="brightness_bar_position">亮度条位置</string>
+    <string name="brightness_bar_left">左侧</string>
+    <string name="brightness_bar_right">右侧</string>
+    <string name="brightness_follow_system">跟随系统</string>
     <string name="language">语言</string>
     <string name="import_rss_source">导入订阅源</string>
     <string name="donate_summary">您的支持是我更新的动力</string>

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

@@ -73,6 +73,17 @@
         <item>chapter</item>
     </string-array>
 
+    <string-array name="brightness_bar_mode_value">
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+    </string-array>
+
+    <string-array name="brightness_bar_position_value">
+        <item>0</item>
+        <item>1</item>
+    </string-array>
+
     <string-array name="language_value">
         <item>auto</item>
         <item>zh</item>

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

@@ -147,6 +147,17 @@
 		<item>@string/adjust_chapter_index</item>
 	</string-array>
 
+	<string-array name="brightness_bar_mode_title">
+		<item>@string/brightness_bar_off</item>
+		<item>@string/brightness_bar_horizontal</item>
+		<item>@string/brightness_bar_vertical</item>
+	</string-array>
+
+	<string-array name="brightness_bar_position_title">
+		<item>@string/brightness_bar_left</item>
+		<item>@string/brightness_bar_right</item>
+	</string-array>
+
 	<string-array name="icon_names" tools:ignore="Typos">
 		<item>iconMain</item>
 		<item>iconWhite</item>

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

@@ -932,6 +932,13 @@
     <string name="wrong_format">Wrong format</string>
     <string name="error">Error</string>
     <string name="show_brightness_view">Show Brightness Widget</string>
+    <string name="brightness_bar_off">Off</string>
+    <string name="brightness_bar_horizontal">Horizontal</string>
+    <string name="brightness_bar_vertical">Vertical</string>
+    <string name="brightness_bar_position">Brightness Bar Position</string>
+    <string name="brightness_bar_left">Left</string>
+    <string name="brightness_bar_right">Right</string>
+    <string name="brightness_follow_system">Follow System</string>
     <string name="language">Language</string>
     <string name="import_rss_source">Import rss source</string>
     <string name="donate_summary">Your donation makes this app better</string>

+ 4 - 2
app/src/main/res/xml/pref_config_read.xml

@@ -93,10 +93,12 @@
             android:title="@string/use_zh_layout"
             app:isBottomBackground="true" />
 
-        <io.legado.app.lib.prefs.SwitchPreference
-            android:defaultValue="true"
+        <io.legado.app.lib.prefs.ListPreference
+            android:defaultValue="1"
             android:key="showBrightnessView"
             android:title="@string/show_brightness_view"
+            android:entries="@array/brightness_bar_mode_title"
+            android:entryValues="@array/brightness_bar_mode_value"
             app:isBottomBackground="true" />
 
         <io.legado.app.lib.prefs.SwitchPreference