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

This commit is contained in:
HapeLee
2026-06-12 03:26:36 +08:00
parent 5ff9f3145a
commit 35875f7dd3
62 changed files with 1496 additions and 680 deletions
@@ -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"
@@ -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) {
@@ -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)
@@ -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()
)
}
}
@@ -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
@@ -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,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
@@ -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)
@@ -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) {
@@ -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) {
@@ -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(
@@ -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 {
}
}
}
}
@@ -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
@@ -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()
}
}
}
@@ -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 {
@@ -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()
@@ -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 {
@@ -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
@@ -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 {
@@ -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
}
}
}
@@ -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,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()) {
@@ -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,
@@ -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")
}
}
}
}
@@ -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)
}
}
@@ -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
@@ -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()
@@ -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
@@ -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(
@@ -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()
@@ -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)) {
@@ -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)
@@ -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" -> {
@@ -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,
@@ -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()
}
}
}
}
}
@@ -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,
@@ -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)
}
}
@@ -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)
}
@@ -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,
@@ -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() }
)
}
}
}
@@ -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()
}
}
@@ -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()
}
}
)
@@ -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
}
}
@@ -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,
""
@@ -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,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,
@@ -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,
@@ -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>()
@@ -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"
@@ -60,6 +60,10 @@ object MainNavigator {
}
}
is MainRouteSearchContent -> {
backStack.add(route)
}
is MainRouteSearch -> {
if (
currentRoute == MainRouteHome ||
@@ -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()
}
}
}
@@ -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,
)
}
}
}
@@ -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),
)
}
@@ -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()