diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 73a1b2366..5a8e52088 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -82,10 +82,6 @@ object PreferKey { const val exportNoChapterName = "exportNoChapterName" const val exportType = "exportType" const val exportPictureFile = "exportPictureFile" - const val changeSourceCheckAuthor = "changeSourceCheckAuthor" - const val changeSourceLoadToc = "changeSourceLoadToc" - const val changeSourceLoadInfo = "changeSourceLoadInfo" - const val changeSourceLoadWordCount = "changeSourceLoadWordCount" const val chineseConverterType = "chineseConverterType" const val launcherIcon = "launcherIcon" const val textSelectAble = "selectText" diff --git a/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt index 7411cd662..76a7cc605 100644 --- a/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt +++ b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt @@ -24,4 +24,11 @@ object LocalPreferencesKeys { val PASSWORD = stringPreferencesKey("password") val PRIVACY_POLICY_OK = booleanPreferencesKey("privacy_policy_ok") val PERMISSION_CHECKED = booleanPreferencesKey("permission_checked") + + // Change source options + val CHANGE_SOURCE_CHECK_AUTHOR = booleanPreferencesKey("changeSourceCheckAuthor") + val CHANGE_SOURCE_LOAD_INFO = booleanPreferencesKey("changeSourceLoadInfo") + val CHANGE_SOURCE_LOAD_TOC = booleanPreferencesKey("changeSourceLoadToc") + val CHANGE_SOURCE_LOAD_WORD_COUNT = booleanPreferencesKey("changeSourceLoadWordCount") + val CHANGE_SOURCE_SEARCH_SCOPE = stringPreferencesKey("changeSourceSearchScope") } diff --git a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt b/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt index 6e3457826..b2b302268 100644 --- a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt @@ -122,6 +122,7 @@ class SearchContentRepository { matches.forEachIndexed { index, match -> val construct = getResultAndQueryIndex(mContent, match.position, match.length) val result = SearchResult( + bookUrl = book.bookUrl, resultCountWithinChapter = index, resultText = construct.second, chapterTitle = chapter.title, diff --git a/app/src/main/java/io/legado/app/domain/model/BookSearchScope.kt b/app/src/main/java/io/legado/app/domain/model/BookSearchScope.kt index 6af0a41cb..8e4667357 100644 --- a/app/src/main/java/io/legado/app/domain/model/BookSearchScope.kt +++ b/app/src/main/java/io/legado/app/domain/model/BookSearchScope.kt @@ -1,8 +1,8 @@ package io.legado.app.domain.model -import com.google.gson.GsonBuilder -import io.legado.app.utils.fromJsonObject import io.legado.app.utils.splitNotBlank +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json data class BookSearchScope(val raw: String) { @@ -39,11 +39,13 @@ data class BookSearchScope(val raw: String) { get() = groups.isEmpty() && sources.isEmpty() } + @Serializable data class ScopeSourceItem( val name: String, val url: String, ) + @Serializable private data class SerializedSearchScope( val type: String = "", val groups: List = emptyList(), @@ -57,7 +59,9 @@ data class BookSearchScope(val raw: String) { return if (selected.isEmpty()) { "" } else { - scopeGson.toJson(SerializedSearchScope(type = TYPE_GROUP, groups = selected)) + scopeJson.encodeToString( + SerializedSearchScope(type = TYPE_GROUP, groups = selected) + ) } } @@ -66,7 +70,9 @@ data class BookSearchScope(val raw: String) { return if (selected.isEmpty()) { "" } else { - scopeGson.toJson(SerializedSearchScope(type = TYPE_SOURCE, sources = selected)) + scopeJson.encodeToString( + SerializedSearchScope(type = TYPE_SOURCE, sources = selected) + ) } } @@ -84,7 +90,9 @@ data class BookSearchScope(val raw: String) { val json = raw.trim() if (!json.startsWith("{") || !json.endsWith("}")) return null - return scopeGson.fromJsonObject(json).getOrNull()?.let { scope -> + return runCatching { + scopeJson.decodeFromString(json) + }.getOrNull()?.let { scope -> when (scope.type) { TYPE_SOURCE -> ParsedSearchScope( sources = scope.sources.filter { it.url.isNotBlank() } @@ -124,7 +132,10 @@ data class BookSearchScope(val raw: String) { private const val TYPE_GROUP = "group" private const val TYPE_SOURCE = "source" - private val scopeGson = GsonBuilder().disableHtmlEscaping().create() + private val scopeJson = Json { + encodeDefaults = false + ignoreUnknownKeys = true + } } } diff --git a/app/src/main/java/io/legado/app/domain/usecase/ChangeSourceSearchUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ChangeSourceSearchUseCase.kt index 7d3bd5bae..408c7466f 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/ChangeSourceSearchUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/ChangeSourceSearchUseCase.kt @@ -9,7 +9,7 @@ import io.legado.app.help.book.BookHelp import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.primaryStr import io.legado.app.help.book.releaseHtmlData -import io.legado.app.help.config.AppConfig +import io.legado.app.ui.book.changesource.ChangeSourceConfig import io.legado.app.help.source.SourceHelp import io.legado.app.model.webBook.WebBook import io.legado.app.ui.book.changesource.ObservableSourceConfig @@ -135,10 +135,10 @@ class ChangeSourceSearchUseCase( fromReadBookActivity: Boolean, contentProcessor: ContentProcessor, ): List { - val checkAuthor = AppConfig.changeSourceCheckAuthor - val loadInfo = AppConfig.changeSourceLoadInfo - val loadToc = AppConfig.changeSourceLoadToc - val loadWordCount = AppConfig.changeSourceLoadWordCount + val checkAuthor = ChangeSourceConfig.checkAuthor + val loadInfo = ChangeSourceConfig.loadInfo + val loadToc = ChangeSourceConfig.loadToc + val loadWordCount = ChangeSourceConfig.loadWordCount val resultBooks = WebBook.searchBookAwait( source, name, diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 83dbcb831..1fbafa865 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -465,12 +465,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { appCtx.putPrefBoolean(PreferKey.parallelExportBook, value) } - var changeSourceCheckAuthor: Boolean - get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor) - set(value) { - appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value) - } - var ttsEngine: String? get() = io.legado.app.ui.config.readConfig.ReadConfig.ttsEngine set(value) { @@ -504,24 +498,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { val autoChangeSource: Boolean get() = ReadConfig.autoChangeSource - var changeSourceLoadInfo: Boolean - get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo) - set(value) { - appCtx.putPrefBoolean(PreferKey.changeSourceLoadInfo, value) - } - - var changeSourceLoadToc: Boolean - get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc) - set(value) { - appCtx.putPrefBoolean(PreferKey.changeSourceLoadToc, value) - } - - var changeSourceLoadWordCount: Boolean - get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadWordCount) - set(value) { - appCtx.putPrefBoolean(PreferKey.changeSourceLoadWordCount, value) - } - var openBookInfoByClickTitle: Boolean get() = appCtx.getPrefBoolean(PreferKey.openBookInfoByClickTitle, true) set(value) { diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt index 2f75a9052..96d5c2601 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceAdapter.kt @@ -11,7 +11,6 @@ import io.legado.app.base.adapter.DiffRecyclerAdapter import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.data.entities.SearchBook import io.legado.app.databinding.ItemChangeSourceBinding -import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.utils.gone import io.legado.app.utils.invisible @@ -91,13 +90,13 @@ class ChangeBookSourceAdapter( binding.ivGood.setImageResource(R.drawable.ic_praise) } - if (AppConfig.changeSourceLoadWordCount && !item.chapterWordCountText.isNullOrBlank()) { + if (ChangeSourceConfig.loadWordCount && !item.chapterWordCountText.isNullOrBlank()) { tvCurrentChapterWordCount.visible() } else { tvCurrentChapterWordCount.gone() } - if (AppConfig.changeSourceLoadWordCount && item.respondTime >= 0) { + if (ChangeSourceConfig.loadWordCount && item.respondTime >= 0) { tvRespondTime.visible() } else { tvRespondTime.gone() diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt index 9a29f0569..adccfda3f 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceComposeViewModel.kt @@ -227,11 +227,13 @@ class ChangeBookSourceComposeViewModel( fun onLoadInfoChange(enabled: Boolean) { if (ChangeSourceConfig.loadInfo == enabled) return ChangeSourceConfig.loadInfo = enabled + refresh() } fun onLoadTocChange(enabled: Boolean) { if (ChangeSourceConfig.loadToc == enabled) return ChangeSourceConfig.loadToc = enabled + refresh() } fun onLoadWordCountChange(enabled: Boolean) { diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt index b50d3d875..6c4d64967 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt @@ -29,7 +29,6 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.databinding.DialogBookChangeSourceBinding -import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.model.ReadBook import io.legado.app.ui.book.search.SearchScope @@ -133,13 +132,13 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b binding.toolBar.menu.applyTint(requireContext()) binding.toolBar.setOnMenuItemClickListener(this) binding.toolBar.menu.findItem(R.id.menu_check_author) - ?.isChecked = AppConfig.changeSourceCheckAuthor + ?.isChecked = ChangeSourceConfig.checkAuthor binding.toolBar.menu.findItem(R.id.menu_load_info) - ?.isChecked = AppConfig.changeSourceLoadInfo + ?.isChecked = ChangeSourceConfig.loadInfo binding.toolBar.menu.findItem(R.id.menu_load_toc) - ?.isChecked = AppConfig.changeSourceLoadToc + ?.isChecked = ChangeSourceConfig.loadToc binding.toolBar.menu.findItem(R.id.menu_load_word_count) - ?.isChecked = AppConfig.changeSourceLoadWordCount + ?.isChecked = ChangeSourceConfig.loadWordCount } private fun initRecyclerView() { @@ -316,23 +315,25 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b override fun onMenuItemClick(item: MenuItem?): Boolean { when (item?.itemId) { R.id.menu_check_author -> { - AppConfig.changeSourceCheckAuthor = !item.isChecked + ChangeSourceConfig.checkAuthor = !item.isChecked item.isChecked = !item.isChecked viewModel.refresh() } R.id.menu_load_info -> { - AppConfig.changeSourceLoadInfo = !item.isChecked + ChangeSourceConfig.loadInfo = !item.isChecked item.isChecked = !item.isChecked + viewModel.refresh() } R.id.menu_load_toc -> { - AppConfig.changeSourceLoadToc = !item.isChecked + ChangeSourceConfig.loadToc = !item.isChecked item.isChecked = !item.isChecked + viewModel.refresh() } R.id.menu_load_word_count -> { - AppConfig.changeSourceLoadWordCount = !item.isChecked + ChangeSourceConfig.loadWordCount = !item.isChecked item.isChecked = !item.isChecked viewModel.onLoadWordCountChecked(item.isChecked) } diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt index 2c976016a..fa6ce94b3 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceViewModel.kt @@ -19,7 +19,6 @@ import io.legado.app.help.book.BookHelp import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.primaryStr import io.legado.app.help.book.releaseHtmlData -import io.legado.app.help.config.AppConfig import io.legado.app.help.config.SourceConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.source.SourceHelp @@ -137,7 +136,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a } }.map { kotlin.runCatching { - val comparator = if (AppConfig.changeSourceLoadWordCount) { + val comparator = if (ChangeSourceConfig.loadWordCount) { wordCountComparator } else { defaultComparator @@ -269,10 +268,10 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a } private suspend fun search(source: BookSource) { - val checkAuthor = AppConfig.changeSourceCheckAuthor - val loadInfo = AppConfig.changeSourceLoadInfo - val loadToc = AppConfig.changeSourceLoadToc - val loadWordCount = AppConfig.changeSourceLoadWordCount + val checkAuthor = ChangeSourceConfig.checkAuthor + val loadInfo = ChangeSourceConfig.loadInfo + val loadToc = ChangeSourceConfig.loadToc + val loadWordCount = ChangeSourceConfig.loadWordCount val resultBooks = WebBook.searchBookAwait( source, name, filter = { fName, fAuthor, _ -> @@ -295,7 +294,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a if (book.tocUrl.isEmpty()) { WebBook.getBookInfoAwait(source, book) } - if (AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount) { + if (ChangeSourceConfig.loadToc || ChangeSourceConfig.loadWordCount) { loadBookToc(source, book) } else { //从详情页里获取最新章节 @@ -315,7 +314,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a } bookMap[book.primaryStr()] = book book.releaseHtmlData() - if (AppConfig.changeSourceLoadWordCount) { + if (ChangeSourceConfig.loadWordCount) { loadBookWordCount(source, book, chapters) } else { val searchBook = book.toSearchBook() @@ -418,13 +417,13 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a else -> searchScope.displayNames.firstOrNull() ?: "" } return if (screenKey.isEmpty()) { - if (AppConfig.changeSourceCheckAuthor) { + if (ChangeSourceConfig.checkAuthor) { appDb.searchBookDao.changeSourceByGroup(name, author, group) } else { appDb.searchBookDao.changeSourceByGroup(name, "", group) } } else { - if (AppConfig.changeSourceCheckAuthor) { + if (ChangeSourceConfig.checkAuthor) { appDb.searchBookDao.changeSourceSearch(name, author, screenKey, group) } else { appDb.searchBookDao.changeSourceSearch(name, "", screenKey, group) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt index 006bbfe3a..71d87ba5e 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceViewModel.kt @@ -133,11 +133,13 @@ class ChangeChapterSourceViewModel( is ChangeChapterSourceIntent.SetLoadInfo -> { ChangeSourceConfig.loadInfo = intent.enabled _uiState.update { it.copy(loadInfo = intent.enabled) } + refreshResults() } is ChangeChapterSourceIntent.SetLoadToc -> { ChangeSourceConfig.loadToc = intent.enabled _uiState.update { it.copy(loadToc = intent.enabled) } + refreshResults() } is ChangeChapterSourceIntent.SetLoadWordCount -> { diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt index f4aa05526..f703c711c 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceConfig.kt @@ -1,35 +1,83 @@ package io.legado.app.ui.book.changesource -import io.legado.app.constant.PreferKey +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.Snapshot +import io.legado.app.data.local.preferences.LocalPreferencesKeys +import io.legado.app.data.local.preferences.LocalPreferencesRepository import io.legado.app.domain.usecase.ChangeSourceMigrationOptions import io.legado.app.ui.config.prefDelegate +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import splitties.init.appCtx object ChangeSourceConfig { - var searchScope by prefDelegate( - key = "changeSourceSearchScope", - defaultValue = "" - ) + private val repo = LocalPreferencesRepository(appCtx) + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - var checkAuthor by prefDelegate( - key = PreferKey.changeSourceCheckAuthor, - defaultValue = false - ) + private val _searchScope = mutableStateOf("") + var searchScope: String + get() = _searchScope.value + set(value) { + if (_searchScope.value != value) { + Snapshot.withMutableSnapshot { _searchScope.value = value } + scope.launch { + repo.updatePreference(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, value) + } + } + } - var loadInfo by prefDelegate( - key = PreferKey.changeSourceLoadInfo, - defaultValue = false - ) + private val _checkAuthor = mutableStateOf(false) + var checkAuthor: Boolean + get() = _checkAuthor.value + set(value) { + if (_checkAuthor.value != value) { + Snapshot.withMutableSnapshot { _checkAuthor.value = value } + scope.launch { + repo.updatePreference(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, value) + } + } + } - var loadToc by prefDelegate( - key = PreferKey.changeSourceLoadToc, - defaultValue = false - ) + private val _loadInfo = mutableStateOf(false) + var loadInfo: Boolean + get() = _loadInfo.value + set(value) { + if (_loadInfo.value != value) { + Snapshot.withMutableSnapshot { _loadInfo.value = value } + scope.launch { + repo.updatePreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, value) + } + } + } - var loadWordCount by prefDelegate( - key = PreferKey.changeSourceLoadWordCount, - defaultValue = false - ) + private val _loadToc = mutableStateOf(false) + var loadToc: Boolean + get() = _loadToc.value + set(value) { + if (_loadToc.value != value) { + Snapshot.withMutableSnapshot { _loadToc.value = value } + scope.launch { + repo.updatePreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, value) + } + } + } + + private val _loadWordCount = mutableStateOf(false) + var loadWordCount: Boolean + get() = _loadWordCount.value + set(value) { + if (_loadWordCount.value != value) { + Snapshot.withMutableSnapshot { _loadWordCount.value = value } + scope.launch { + repo.updatePreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, value) + } + } + } var migrateChapters by prefDelegate( key = "migrateChapters", @@ -64,6 +112,44 @@ object ChangeSourceConfig { defaultValue = false ) + init { + runBlocking(Dispatchers.IO) { + Snapshot.withMutableSnapshot { + _searchScope.value = + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, "").first() + _checkAuthor.value = + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, false).first() + _loadInfo.value = + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, false).first() + _loadToc.value = + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, false).first() + _loadWordCount.value = + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, false).first() + } + } + + scope.launch { + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_SEARCH_SCOPE, "") + .collect { Snapshot.withMutableSnapshot { _searchScope.value = it } } + } + scope.launch { + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_CHECK_AUTHOR, false) + .collect { Snapshot.withMutableSnapshot { _checkAuthor.value = it } } + } + scope.launch { + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_INFO, false) + .collect { Snapshot.withMutableSnapshot { _loadInfo.value = it } } + } + scope.launch { + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_TOC, false) + .collect { Snapshot.withMutableSnapshot { _loadToc.value = it } } + } + scope.launch { + repo.getPreference(LocalPreferencesKeys.CHANGE_SOURCE_LOAD_WORD_COUNT, false) + .collect { Snapshot.withMutableSnapshot { _loadWordCount.value = it } } + } + } + fun getMigrationOptions(): ChangeSourceMigrationOptions { return ChangeSourceMigrationOptions( migrateChapters = migrateChapters, diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt index 119986f5b..579608c2a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt @@ -40,6 +40,7 @@ sealed interface ReadBookMenuRoute { data object ReadAloud : ReadBookMenuRoute data object AutoRead : ReadBookMenuRoute data object PaddingConfig : ReadBookMenuRoute + data object HeaderFooterConfig : ReadBookMenuRoute data class Bookmark(val bookmark: io.legado.app.data.entities.Bookmark) : ReadBookMenuRoute } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt index 72c02227a..8342a8434 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt @@ -717,7 +717,7 @@ class ReadBookController( // Chapter not loaded — open it, then mark in the success callback ReadBook.openChapter( effect.chapterIndex, - effect.result.queryIndexInChapter + 0 ) { val tc = ReadBook.curTextChapter ?: return@openChapter val query = effect.result.query diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt index 41a8c405f..dfb45706c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt @@ -139,6 +139,7 @@ import io.legado.app.help.config.ReadStyleResolver import io.legado.app.ui.animation.DampedDragAnimation import io.legado.app.ui.animation.InteractiveHighlight import io.legado.app.ui.book.read.sheet.AutoReadContent +import io.legado.app.ui.book.read.sheet.HeaderFooterPage import io.legado.app.ui.book.read.sheet.PaddingConfigContent import io.legado.app.ui.book.read.sheet.ReadAloudContent import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo @@ -186,7 +187,7 @@ fun ReadBookMenuBar( } else { ReadBookMenuContent.Route(currentRoute) } - val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig + val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig || currentRoute == ReadBookMenuRoute.HeaderFooterConfig var readStylePage by remember { mutableIntStateOf(0) } LaunchedEffect(currentRoute) { if (currentRoute != ReadBookMenuRoute.ReadStyle) { @@ -392,7 +393,7 @@ private fun ReadBookMenuSurface( is ReadBookMenuContent.Route -> contentTarget.route } val expanded = route != ReadBookMenuRoute.Main - val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig + val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig || route == ReadBookMenuRoute.HeaderFooterConfig val density = LocalDensity.current val windowSize = LocalWindowInfo.current.containerSize var surfaceHeightPx by remember { mutableIntStateOf(0) } @@ -400,18 +401,6 @@ 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 } @@ -518,8 +507,7 @@ private fun ReadBookMenuSurface( .onSizeChanged { surfaceHeightPx = it.height } .offset { val dialogLiftPx = ((windowSize.height - surfaceHeightPx) / 2f) * morphProgress - val liftPx = dialogLiftPx + headerFooterLift.toPx() - IntOffset(x = 0, y = -liftPx.roundToInt()) + IntOffset(x = 0, y = -dialogLiftPx.roundToInt()) } .then( if (useLiquidGlass) { @@ -620,6 +608,9 @@ private fun ReadBookMenuSurface( onOpenPaddingConfig = { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig)) }, + onOpenHeaderFooterConfig = { + onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.HeaderFooterConfig)) + }, onOpenMoreConfig = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreConfig)) }, @@ -659,6 +650,20 @@ private fun ReadBookMenuSurface( } } + ReadBookMenuRoute.HeaderFooterConfig -> { + ReadBookMenuRoutePage( + title = stringResource(R.string.header_footer), + maxHeight = maxHeight, + scrollContent = false, + bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp, + onBack = { onIntent(ReadBookIntent.ReadMenuBack) }, + ) { + HeaderFooterPage( + onIntent = onIntent, + ) + } + } + ReadBookMenuRoute.TextTitle -> { ReadBookMenuRoutePage( title = stringResource(R.string.read_config_text_effects), diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt index e8a4aa9a1..ef879ee04 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt @@ -31,6 +31,7 @@ import io.legado.app.R import io.legado.app.constant.AppLog import io.legado.app.constant.ReadMenuBlurMode import io.legado.app.help.IntentHelp +import io.legado.app.model.ReadBook 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 @@ -383,6 +384,10 @@ fun ReadBookRouteScreen( lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { SearchContentResult.results.collect { result -> effectsReady.await() + if (result.bookUrl != ReadBook.book?.bookUrl) { + SearchContentResult.resetReplayCache() + return@collect + } viewModel.onIntent( ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query) ) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index e3110f726..2f720bd3e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -2241,50 +2241,21 @@ class ReadBookViewModel( return arrayOf(-1, 0, 0, 0, 0, 0) } - var pageIndex = 0 - var length = pages[pageIndex].text.length - while (length < contentPosition && pageIndex + 1 < pages.size) { - pageIndex += 1 - length += pages[pageIndex].text.length - } - if (length < contentPosition) return arrayOf(-1, 0, 0, 0, 0, 0) - - val currentPage = pages[pageIndex] - val lines = currentPage.lines - if (lines.isEmpty()) return arrayOf(-1, 0, 0, 0, 0, 0) - - var lineIndex = 0 - var currentLine = lines[lineIndex] - length = length - currentPage.text.length + currentLine.text.length - if (currentLine.isParagraphEnd) length++ - while (length <= contentPosition && lineIndex + 1 < lines.size) { - lineIndex += 1 - currentLine = lines[lineIndex] - length += currentLine.text.length - if (currentLine.isParagraphEnd) length++ - } - - var currentLineLength = currentLine.text.length - if (currentLine.isParagraphEnd) currentLineLength++ - length -= currentLineLength - - val charIndex = contentPosition - length - var addLine = 0 - var charIndex2 = 0 - if ((charIndex + queryLength) > currentLineLength) { - addLine = 1 - charIndex2 = charIndex + queryLength - currentLineLength - 1 - } - if ((lineIndex + addLine + 1) > currentPage.lines.size) { - addLine = -1 - charIndex2 = charIndex + queryLength - currentLineLength - 1 - } - return when (addLine) { - 0 -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex, charIndex + queryLength - 1) - 1 -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex + 1, charIndex2) - -1 -> arrayOf(pageIndex, lineIndex, charIndex, 1, 0, charIndex2) - else -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex, charIndex) - } + val start = findSearchTextPoint(pages, contentPosition, preferPreviousLine = false) + ?: return arrayOf(-1, 0, 0, 0, 0, 0) + val end = findSearchTextPoint( + pages, + contentPosition + queryLength - 1, + preferPreviousLine = true + ) ?: start + return arrayOf( + start.pageIndex, + start.lineIndex, + start.charIndex, + end.pageIndex - start.pageIndex, + end.lineIndex, + end.charIndex + ) } private fun findSearchResultMatch( @@ -2293,6 +2264,26 @@ class ReadBookViewModel( query: String, ): Pair? { if (query.isEmpty()) return null + val directLength = if (searchResult.matchLength > 0) searchResult.matchLength else query.length + val directIndex = searchResult.queryIndexInChapter + if (directIndex >= 0 && directIndex + directLength <= content.length) { + val directMatch = if (searchResult.isRegex) { + runCatching { + Regex(query).matches(content.substring(directIndex, directIndex + directLength)) + }.getOrDefault(false) + } else { + content.regionMatches( + directIndex, + query, + 0, + query.length, + ignoreCase = false + ) + } + if (directMatch) { + return directIndex to directLength + } + } if (searchResult.isRegex) { return runCatching { Regex(query).findAll(content) @@ -2311,6 +2302,43 @@ class ReadBookViewModel( return index.takeIf { it >= 0 }?.let { it to query.length } } + private fun findSearchTextPoint( + pages: List, + contentPosition: Int, + preferPreviousLine: Boolean, + ): SearchTextPoint? { + var fallback: SearchTextPoint? = null + pages.forEachIndexed { pageIndex, page -> + page.lines.forEachIndexed { lineIndex, line -> + if (line.columns.isEmpty()) return@forEachIndexed + val lineStart = line.chapterPosition + val lineEndExclusive = lineStart + line.charSize + if (contentPosition in lineStart until lineEndExclusive) { + return SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = (contentPosition - lineStart).coerceIn(0, line.columns.lastIndex) + ) + } + if (preferPreviousLine && line.isParagraphEnd && contentPosition == lineEndExclusive) { + return SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = line.columns.lastIndex + ) + } + if (contentPosition >= lineEndExclusive) { + fallback = SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = line.columns.lastIndex + ) + } + } + } + return fallback + } + /** * Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult] * so the Controller can navigate and highlight. @@ -3572,6 +3600,12 @@ private const val TOOL_BUTTON_PREFS = "tool_button_config" private const val TOOL_BUTTON_KEY = "tool_buttons" private const val DEFAULT_ENABLED_BUTTON_COUNT = 5 +private data class SearchTextPoint( + val pageIndex: Int, + val lineIndex: Int, + val charIndex: Int, +) + private fun Int.coerceSearchResultIndex(resultSize: Int): Int { return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt index 086fefb4c..0919bcdba 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt @@ -1,25 +1,35 @@ package io.legado.app.ui.book.read.sheet +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer 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.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.TextFields +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf @@ -32,11 +42,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -46,6 +54,7 @@ import io.legado.app.data.repository.ReadSettingsRepository import io.legado.app.help.config.ReadBookConfig import io.legado.app.ui.book.read.ConfigUpdate import io.legado.app.ui.book.read.ReadBookIntent +import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.FontSelectSheet import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem @@ -106,6 +115,42 @@ internal fun HeaderFooterPage( var colorPickerInitial by remember { mutableIntStateOf(0) } var showFontSelect by remember { mutableStateOf(false) } + var expandHeaderPadding by remember { mutableStateOf(false) } + var expandFooterPadding by remember { mutableStateOf(false) } + + val headerScrollState = rememberScrollState() + val footerScrollState = rememberScrollState() + + LaunchedEffect(expandHeaderPadding, headerScrollState.maxValue) { + if (expandHeaderPadding) { + headerScrollState.scrollTo(headerScrollState.maxValue) + } + } + + LaunchedEffect(expandFooterPadding, footerScrollState.maxValue) { + if (expandFooterPadding) { + footerScrollState.scrollTo(footerScrollState.maxValue) + } + } + + DisposableEffect(Unit) { + onDispose { + onIntent(ReadBookIntent.SaveReadStyleConfig) + } + } + + // Header padding state + var headerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingTop.toFloat()) } + var headerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingBottom.toFloat()) } + var headerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingLeft.toFloat()) } + var headerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingRight.toFloat()) } + + // Footer padding state + var footerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingTop.toFloat()) } + var footerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingBottom.toFloat()) } + var footerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingLeft.toFloat()) } + var footerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingRight.toFloat()) } + val tipNames = remember { ReadBookConfig.tipNames } val tipValues = remember { ReadBookConfig.tipValues } @@ -153,7 +198,7 @@ internal fun HeaderFooterPage( scope.launch { pagerState.animateScrollToPage( page = index, - animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) ) } }, @@ -181,7 +226,7 @@ internal fun HeaderFooterPage( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(headerScrollState), ) { TinySwitchSettingItem( title = stringResource(R.string.showLine), @@ -252,6 +297,56 @@ internal fun HeaderFooterPage( showColorPicker = true }, ) + Spacer(Modifier.height(8.dp)) + TinyClickableSettingItem( + title = stringResource(R.string.padding), + description = stringResource( + R.string.padding_format, + headerPaddingTop.toInt(), + headerPaddingBottom.toInt(), + headerPaddingLeft.toInt(), + headerPaddingRight.toInt(), + ), + trailingContent = { + Icon( + imageVector = if (expandHeaderPadding) Icons.Default.ExpandMore else Icons.Default.ChevronRight, + contentDescription = null, + tint = LegadoTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + }, + onClick = { + expandHeaderPadding = !expandHeaderPadding + }, + ) + AnimatedVisibility( + visible = expandHeaderPadding, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + PaddingSliders( + top = headerPaddingTop, bottom = headerPaddingBottom, + left = headerPaddingLeft, right = headerPaddingRight, + onTopChange = { + headerPaddingTop = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingTop(it.toInt()))) + }, + onBottomChange = { + headerPaddingBottom = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingBottom(it.toInt()))) + }, + onLeftChange = { + headerPaddingLeft = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingLeft(it.toInt()))) + }, + onRightChange = { + headerPaddingRight = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingRight(it.toInt()))) + }, + ) + } + } } } @@ -261,7 +356,7 @@ internal fun HeaderFooterPage( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(footerScrollState), ) { TinySwitchSettingItem( title = stringResource(R.string.showLine), @@ -332,6 +427,56 @@ internal fun HeaderFooterPage( showColorPicker = true }, ) + Spacer(Modifier.height(8.dp)) + TinyClickableSettingItem( + title = stringResource(R.string.padding), + description = stringResource( + R.string.padding_format, + footerPaddingTop.toInt(), + footerPaddingBottom.toInt(), + footerPaddingLeft.toInt(), + footerPaddingRight.toInt(), + ), + trailingContent = { + Icon( + imageVector = if (expandFooterPadding) Icons.Default.ExpandMore else Icons.Default.ChevronRight, + contentDescription = null, + tint = LegadoTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + }, + onClick = { + expandFooterPadding = !expandFooterPadding + }, + ) + AnimatedVisibility( + visible = expandFooterPadding, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + PaddingSliders( + top = footerPaddingTop, bottom = footerPaddingBottom, + left = footerPaddingLeft, right = footerPaddingRight, + onTopChange = { + footerPaddingTop = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingTop(it.toInt()))) + }, + onBottomChange = { + footerPaddingBottom = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingBottom(it.toInt()))) + }, + onLeftChange = { + footerPaddingLeft = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingLeft(it.toInt()))) + }, + onRightChange = { + footerPaddingRight = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingRight(it.toInt()))) + }, + ) + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/PaddingConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/PaddingConfigSheet.kt index 27f3040bb..dfb3a18c5 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/PaddingConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/PaddingConfigSheet.kt @@ -3,18 +3,11 @@ package io.legado.app.ui.book.read.sheet import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -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 -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -24,8 +17,6 @@ import io.legado.app.ui.book.read.ConfigUpdate import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem -import io.legado.app.ui.widget.components.tabRow.CardTabRow -import kotlinx.coroutines.launch @Composable fun PaddingConfigSheet( @@ -58,122 +49,35 @@ fun PaddingConfigContent( var paddingBottom by remember { mutableFloatStateOf(ReadBookConfig.paddingBottom.toFloat()) } var paddingLeft by remember { mutableFloatStateOf(ReadBookConfig.paddingLeft.toFloat()) } var paddingRight by remember { mutableFloatStateOf(ReadBookConfig.paddingRight.toFloat()) } - // Header padding - var headerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingTop.toFloat()) } - var headerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingBottom.toFloat()) } - var headerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingLeft.toFloat()) } - var headerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingRight.toFloat()) } - // Footer padding - var footerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingTop.toFloat()) } - var footerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingBottom.toFloat()) } - var footerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingLeft.toFloat()) } - var footerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingRight.toFloat()) } - - val scope = rememberCoroutineScope() - val tabTitles = listOf( - stringResource(R.string.header), - stringResource(R.string.main_body), - stringResource(R.string.footer), - ) - val pagerState = rememberPagerState(pageCount = { 3 }) - var selectedTab by remember { mutableIntStateOf(0) } - - LaunchedEffect(pagerState) { - snapshotFlow { pagerState.settledPage }.collect { selectedTab = it } - } Column( modifier = modifier.fillMaxWidth(), ) { - CardTabRow( - tabTitles = tabTitles, - selectedTabIndex = selectedTab, - onTabSelected = { index -> - selectedTab = index - scope.launch { pagerState.animateScrollToPage(index) } + PaddingSliders( + top = paddingTop, bottom = paddingBottom, + left = paddingLeft, right = paddingRight, + onTopChange = { + paddingTop = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingTop(it.toInt()))) + }, + onBottomChange = { + paddingBottom = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingBottom(it.toInt()))) + }, + onLeftChange = { + paddingLeft = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingLeft(it.toInt()))) + }, + onRightChange = { + paddingRight = it + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingRight(it.toInt()))) }, - modifier = Modifier.padding(bottom = 8.dp), ) - HorizontalPager( - state = pagerState, - modifier = Modifier.fillMaxWidth(), - ) { page -> - when (page) { - 0 -> Column(modifier = Modifier.padding(vertical = 8.dp)) { - PaddingSliders( - top = headerPaddingTop, bottom = headerPaddingBottom, - left = headerPaddingLeft, right = headerPaddingRight, - onTopChange = { - headerPaddingTop = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingTop(it.toInt()))) - }, - onBottomChange = { - headerPaddingBottom = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingBottom(it.toInt()))) - }, - onLeftChange = { - headerPaddingLeft = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingLeft(it.toInt()))) - }, - onRightChange = { - headerPaddingRight = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingRight(it.toInt()))) - }, - ) - } - - 1 -> Column(modifier = Modifier.padding(vertical = 8.dp)) { - PaddingSliders( - top = paddingTop, bottom = paddingBottom, - left = paddingLeft, right = paddingRight, - onTopChange = { - paddingTop = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingTop(it.toInt()))) - }, - onBottomChange = { - paddingBottom = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingBottom(it.toInt()))) - }, - onLeftChange = { - paddingLeft = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingLeft(it.toInt()))) - }, - onRightChange = { - paddingRight = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingRight(it.toInt()))) - }, - ) - } - - 2 -> Column(modifier = Modifier.padding(vertical = 8.dp)) { - PaddingSliders( - top = footerPaddingTop, bottom = footerPaddingBottom, - left = footerPaddingLeft, right = footerPaddingRight, - onTopChange = { - footerPaddingTop = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingTop(it.toInt()))) - }, - onBottomChange = { - footerPaddingBottom = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingBottom(it.toInt()))) - }, - onLeftChange = { - footerPaddingLeft = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingLeft(it.toInt()))) - }, - onRightChange = { - footerPaddingRight = it - onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingRight(it.toInt()))) - }, - ) - } - } - } } } @Composable -private fun PaddingSliders( +internal fun PaddingSliders( top: Float, bottom: Float, left: Float, right: Float, onTopChange: (Float) -> Unit, onBottomChange: (Float) -> Unit, onLeftChange: (Float) -> Unit, onRightChange: (Float) -> Unit, diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt index c606d3ebd..6d874e8e0 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.launch @Composable fun ReadStyleContent( onOpenPaddingConfig: () -> Unit, + onOpenHeaderFooterConfig: () -> Unit, onOpenMoreConfig: () -> Unit, onOpenBgTextConfig: (Int) -> Unit, onOpenTextTitle: () -> Unit, @@ -51,7 +52,7 @@ fun ReadStyleContent( styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(), ) { val scope = rememberCoroutineScope() - val pagerState = rememberPagerState(pageCount = { 3 }) + val pagerState = rememberPagerState(pageCount = { 2 }) var currentPage by remember { mutableIntStateOf(0) } val pageHeights = remember { mutableStateMapOf() } @@ -105,10 +106,6 @@ fun ReadStyleContent( bottomBarButtons = bottomBarButtons, onIntent = onIntent, ) - - 2 -> HeaderFooterPage( - onIntent = onIntent, - ) } } } @@ -123,15 +120,17 @@ fun ReadStyleContent( tabTitles = tabTitles, selectedTabIndex = currentPage, onTabSelected = { index -> - if (index < 3) { - scope.launch { - pagerState.animateScrollToPage( - page = index, - animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) - ) + when (index) { + 0, 1 -> { + scope.launch { + pagerState.animateScrollToPage( + page = index, + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) + ) + } } - } else { - onOpenMoreConfig() + 2 -> onOpenHeaderFooterConfig() + 3 -> onOpenMoreConfig() } }, modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp), diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt index 572759a08..34677d596 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt @@ -128,7 +128,7 @@ internal fun SystemMenuPage( scope.launch { pagerState.animateScrollToPage( page = index, - animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) ) } }, diff --git a/app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt b/app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt index 8da45444b..37fd8f475 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/ScopeSelectSheet.kt @@ -25,6 +25,7 @@ import io.legado.app.R import io.legado.app.data.entities.BookSourcePart import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.SearchBar +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.series.MediumPlainButton import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.icon.AppIcons @@ -46,9 +47,20 @@ fun ScopeSelectSheet( isSourceScope: Boolean = false, title: String = stringResource(R.string.search_select_group), onConfirm: (() -> Unit)? = null, + onApplyScope: ((ScopeSelection) -> Unit)? = null, ) { var scopeSheetTab by rememberSaveable(show) { mutableIntStateOf(if (isSourceScope) 1 else 0) } var filterText by rememberSaveable(show) { mutableStateOf("") } + var draftIsAll by remember(show, isAll) { mutableStateOf(isAll) } + var draftIsSourceScope by remember(show, isSourceScope) { mutableStateOf(isSourceScope) } + var draftGroups by remember(show, selectedGroups) { mutableStateOf(selectedGroups.toSet()) } + var draftSourceUrls by remember(show, selectedSources) { mutableStateOf(selectedSources.toSet()) } + + val useDraftSelection = onApplyScope != null + val currentIsAll = if (useDraftSelection) draftIsAll else isAll + val currentIsSourceScope = if (useDraftSelection) draftIsSourceScope else isSourceScope + val currentGroups = if (useDraftSelection) draftGroups else selectedGroups + val currentSourceUrls = if (useDraftSelection) draftSourceUrls else selectedSources val filteredGroups = remember(groups, filterText) { if (filterText.isBlank()) groups else groups.filter { it.contains(filterText, ignoreCase = true) } @@ -87,11 +99,18 @@ fun ScopeSelectSheet( SelectionItemCard( title = stringResource(R.string.all_source), - isSelected = isAll, + isSelected = currentIsAll, containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f), inSelectionMode = true, onToggleSelection = { - onSelectAll() + if (useDraftSelection) { + draftIsAll = true + draftIsSourceScope = false + draftGroups = emptySet() + draftSourceUrls = emptySet() + } else { + onSelectAll() + } } ) @@ -113,14 +132,27 @@ fun ScopeSelectSheet( verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(filteredGroups, key = { it }) { groupName -> - val selected = !isSourceScope && selectedGroups.contains(groupName) + val selected = !currentIsSourceScope && currentGroups.contains(groupName) SelectionItemCard( title = groupName, isSelected = selected, containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f), inSelectionMode = true, onToggleSelection = { - onToggleGroup(groupName) + if (useDraftSelection) { + val next = currentGroups.toMutableSet() + if (!currentIsSourceScope && next.contains(groupName)) { + next.remove(groupName) + } else { + next.add(groupName) + } + draftGroups = next + draftSourceUrls = emptySet() + draftIsSourceScope = false + draftIsAll = next.isEmpty() + } else { + onToggleGroup(groupName) + } } ) } @@ -142,7 +174,7 @@ fun ScopeSelectSheet( verticalArrangement = Arrangement.spacedBy(4.dp) ) { items(filteredSources, key = { it.bookSourceUrl }) { source -> - val selected = selectedSources.contains(source.bookSourceUrl) + val selected = currentSourceUrls.contains(source.bookSourceUrl) SelectionItemCard( title = source.bookSourceName, subtitle = source.bookSourceGroup?.takeIf { group -> group.isNotBlank() }, @@ -150,7 +182,20 @@ fun ScopeSelectSheet( isSelected = selected, inSelectionMode = true, onToggleSelection = { - onToggleSource(source) + if (useDraftSelection) { + val next = currentSourceUrls.toMutableSet() + if (next.contains(source.bookSourceUrl)) { + next.remove(source.bookSourceUrl) + } else { + next.add(source.bookSourceUrl) + } + draftSourceUrls = next + draftGroups = emptySet() + draftIsSourceScope = next.isNotEmpty() + draftIsAll = next.isEmpty() + } else { + onToggleSource(source) + } } ) } @@ -158,7 +203,36 @@ fun ScopeSelectSheet( } } + if (onApplyScope != null) { + Spacer(modifier = Modifier.height(16.dp)) + ConfirmDismissButtonsRow( + onDismiss = onDismissRequest, + onConfirm = { + onApplyScope( + ScopeSelection( + groupNames = if (!draftIsSourceScope) draftGroups.toList() else emptyList(), + sources = if (draftIsSourceScope) { + sources.filter { draftSourceUrls.contains(it.bookSourceUrl) } + } else { + emptyList() + }, + isSourceScope = draftIsSourceScope, + ) + ) + onDismissRequest() + }, + dismissText = stringResource(R.string.cancel), + confirmText = stringResource(R.string.confirm), + ) + } + Spacer(modifier = Modifier.height(20.dp)) } } } + +data class ScopeSelection( + val groupNames: List, + val sources: List, + val isSourceScope: Boolean, +) diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt index cdd7cdbaa..5569fcc4b 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt @@ -88,6 +88,11 @@ sealed interface SearchIntent { data class ToggleSourceType(val type: Int) : SearchIntent data object ClearAllSourceTypes : SearchIntent data object SelectAllScope : SearchIntent + data class ApplyScopeSelection( + val groupNames: List, + val sources: List, + val isSourceScope: Boolean, + ) : SearchIntent data class ToggleScopeGroup(val groupName: String) : SearchIntent data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent data class RemoveScopeItem(val scopeName: String) : SearchIntent diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt index d9185f2f6..3e60b12ba 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt @@ -70,7 +70,7 @@ data class SearchScope(private var scope: String) { if (scope.isEmpty()) { return appCtx.getString(R.string.all_source) } - return scope + return parsedScope().groupNames.joinToString(",") } /** diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt index cf98fe7ad..a65fa2883 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt @@ -112,6 +112,7 @@ fun SearchScreen( val lifecycleOwner = LocalLifecycleOwner.current var queryInput by rememberSaveable { mutableStateOf(state.query) } var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf(null) } + var keepResultsPinnedToTop by rememberSaveable { mutableStateOf(true) } val showSuggestionPanel = state.showSuggestions val latestQuery by rememberUpdatedState(state.query) val scrollBehavior = if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) { @@ -243,6 +244,37 @@ fun SearchScreen( } } + LaunchedEffect(state.committedQuery) { + keepResultsPinnedToTop = true + } + + LaunchedEffect(isSourceGroupedMode, listState, groupedListState) { + snapshotFlow { + val activeState = if (isSourceGroupedMode) groupedListState else listState + Triple( + activeState.firstVisibleItemIndex, + activeState.firstVisibleItemScrollOffset, + activeState.isScrollInProgress + ) + }.collect { (index, offset, isScrollInProgress) -> + if (index == 0 && offset == 0) { + keepResultsPinnedToTop = true + } else if (isScrollInProgress) { + keepResultsPinnedToTop = false + } + } + } + + val firstResultKey = state.results.firstOrNull()?.let { + "${it.book.origin}:${it.book.bookUrl}" + } + LaunchedEffect(firstResultKey, state.results.size, state.isSearching) { + if (state.isSearching && keepResultsPinnedToTop && state.results.isNotEmpty()) { + listState.scrollToItem(0) + groupedListState.scrollToItem(0) + } + } + val submitSearch: (String) -> Unit = { rawQuery -> val normalized = rawQuery.trim() if (normalized.isNotBlank()) { @@ -611,6 +643,15 @@ fun SearchScreen( onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) }, isSourceScope = state.isSourceScope, onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) }, + onApplyScope = { selection -> + viewModel.onIntent( + SearchIntent.ApplyScopeSelection( + groupNames = selection.groupNames, + sources = selection.sources, + isSourceScope = selection.isSourceScope, + ) + ) + }, ) AppModalBottomSheet( diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index 2b3865185..dbb5dd587 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -211,6 +211,7 @@ class SearchViewModel( syncScopeState(restartSearch = true, oldScope = oldScope) } + is SearchIntent.ApplyScopeSelection -> applyScopeSelection(intent) is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName) is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source) is SearchIntent.RemoveScopeItem -> { @@ -221,6 +222,7 @@ class SearchViewModel( } is SearchIntent.SetMatchMode -> { + _uiState.update { it.copy(matchMode = intent.mode) } viewModelScope.launch { localPreferencesRepository.updatePreference( LocalPreferencesKeys.MATCH_MODE, intent.mode.value @@ -427,7 +429,6 @@ class SearchViewModel( it.copy( query = query, showSuggestions = showSuggestions, - isManualStop = false, emptyScopeAction = null, ) } @@ -657,6 +658,7 @@ class SearchViewModel( _uiState.update { it.copy(emptyScopeAction = null) } if (action.wasMatchMode == MatchMode.EXACT) { + _uiState.update { it.copy(matchMode = MatchMode.DEFAULT) } viewModelScope.launch { localPreferencesRepository.updatePreference( LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value @@ -672,12 +674,29 @@ class SearchViewModel( } private fun restartCommittedSearchIfNeeded() { - val committed = _uiState.value.committedQuery - if (committed.isNotBlank()) { + val state = _uiState.value + val committed = state.committedQuery + if ( + committed.isNotBlank() && + state.query.trim() == committed && + !state.showSuggestions && + !state.isManualStop + ) { submitSearch(committed) } } + private fun applyScopeSelection(intent: SearchIntent.ApplyScopeSelection) { + val oldScope = searchScope.toString() + when { + intent.isSourceScope -> searchScope.updateSources(intent.sources) + intent.groupNames.isNotEmpty() -> searchScope.update(intent.groupNames) + else -> searchScope.update("") + } + persistSearchScope() + syncScopeState(restartSearch = true, oldScope = oldScope) + } + private fun syncScopeState( restartSearch: Boolean = false, oldScope: String? = null, diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt index c996e410b..6fac850b9 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.searchContent +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow object SearchContentResult { data class Result( + val bookUrl: String, val searchResults: List, val index: Int, val query: String, @@ -25,6 +27,7 @@ object SearchContentResult { _results.tryEmit(result) } + @OptIn(ExperimentalCoroutinesApi::class) fun resetReplayCache() { _results.resetReplayCache() } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt index 31b08a00e..324f9eab8 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -195,6 +195,7 @@ class SearchContentViewModel( if (index < 0) return false SearchContentResult.emitResult( SearchContentResult.Result( + bookUrl = bookUrl, searchResults = results, index = index, query = searchResult.query diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt index 8fcb1e904..eee4acc5d 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt @@ -11,6 +11,7 @@ import android.text.style.UnderlineSpan import io.legado.app.help.config.AppConfig data class SearchResult( + val bookUrl: String = "", val resultCount: Int = 0, val resultCountWithinChapter: Int = 0, val resultText: String = "", diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 59409e3c4..707ea2988 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import androidx.navigation3.runtime.NavKey @@ -67,6 +68,12 @@ import kotlin.coroutines.suspendCoroutine open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { companion object { + private const val KEY_RESTORE_READ_ROUTE = "restoreReadRoute" + private const val KEY_RESTORE_READ_BOOK_URL = "restoreReadBookUrl" + private const val KEY_RESTORE_READ_ALOUD = "restoreReadAloud" + private const val KEY_RESTORE_READ_IN_BOOKSHELF = "restoreReadInBookshelf" + private const val KEY_RESTORE_READ_CHAPTER_CHANGED = "restoreReadChapterChanged" + @Volatile var hasActiveReadBookRoute: Boolean = false @@ -143,10 +150,15 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { private val routeEvents = MutableSharedFlow(extraBufferCapacity = 1) private var bookInfoVariableSetter: ((String, String?) -> Unit)? = null private var shouldApplyDefaultToRead = true + private var restoredReadBookRoute: MainRouteReadBook? = null + private var latestBackStack: List = emptyList() internal var activeReadBookInputHandler: ReadBookInputHandler? = null + internal var activeReadBookRoute: MainRouteReadBook? = null + override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() shouldApplyDefaultToRead = savedInstanceState == null + restoredReadBookRoute = savedInstanceState?.restoreReadBookRoute() super.onCreate(savedInstanceState) if (checkStartupRoute()) return @@ -175,6 +187,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + if (!intent.hasExplicitStartRoute()) return routeEvents.tryEmit(MainNavigator.resolveStartRoute(intent)) } @@ -194,12 +207,21 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { } val startRoutes = remember { - resolveInitialStartRoutes( - resolved = MainNavigator.resolveStartRoute(intent), - defaultToRead = OtherConfig.defaultToRead, - isFreshCreate = shouldApplyDefaultToRead, - ).toTypedArray() + val resolved = MainNavigator.resolveStartRoute(intent) + val hasExplicitStartRoute = intent?.hasExplicitStartRoute() == true + when { + !hasExplicitStartRoute && restoredReadBookRoute != null -> { + arrayOf(MainRouteHome, restoredReadBookRoute!!) + } + shouldApplyDefaultToRead && OtherConfig.defaultToRead && resolved == MainRouteHome -> { + arrayOf(MainRouteHome, MainRouteReadBook()) + } + else -> { + arrayOf(resolved) + } + } } + latestBackStack = startRoutes.toList() val backStack = rememberNavBackStack(*startRoutes) SideEffect { @@ -212,6 +234,11 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { } } + LaunchedEffect(backStack) { + snapshotFlow { backStack.toList() } + .collect { latestBackStack = it } + } + SharedTransitionLayout { NavDisplay( backStack = backStack, @@ -374,6 +401,29 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { if (AppConfig.autoRefreshBook) { outState.putBoolean("isAutoRefreshedBook", true) } + val readRoute = latestBackStack.lastOrNull() as? MainRouteReadBook + ?: activeReadBookRoute + if (readRoute != null) { + outState.putBoolean(KEY_RESTORE_READ_ROUTE, true) + outState.putString(KEY_RESTORE_READ_BOOK_URL, readRoute.bookUrl) + outState.putBoolean(KEY_RESTORE_READ_ALOUD, readRoute.readAloud) + outState.putBoolean(KEY_RESTORE_READ_IN_BOOKSHELF, readRoute.inBookshelf) + outState.putBoolean(KEY_RESTORE_READ_CHAPTER_CHANGED, readRoute.chapterChanged) + } + } + + private fun Bundle.restoreReadBookRoute(): MainRouteReadBook? { + if (!getBoolean(KEY_RESTORE_READ_ROUTE, false)) return null + return MainRouteReadBook( + bookUrl = getString(KEY_RESTORE_READ_BOOK_URL), + readAloud = getBoolean(KEY_RESTORE_READ_ALOUD, false), + inBookshelf = getBoolean(KEY_RESTORE_READ_IN_BOOKSHELF, true), + chapterChanged = getBoolean(KEY_RESTORE_READ_CHAPTER_CHANGED, false), + ) + } + + private fun Intent.hasExplicitStartRoute(): Boolean { + return hasExtra(MainIntent.EXTRA_START_ROUTE) } override fun dispatchKeyEvent(event: KeyEvent): Boolean { @@ -438,18 +488,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { } -internal fun resolveInitialStartRoutes( - resolved: NavKey, - defaultToRead: Boolean, - isFreshCreate: Boolean, -): List { - return if (isFreshCreate && defaultToRead && resolved == MainRouteHome) { - listOf(MainRouteHome, MainRouteReadBook()) - } else { - listOf(resolved) - } -} - class LauncherW : MainActivity() class Launcher1 : MainActivity() class Launcher2 : MainActivity() diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt index 464cac2bb..7ee251c06 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt @@ -310,6 +310,7 @@ fun MainActivity.mainEntryProvider( DisposableEffect(controller, lifecycleOwner, route.readAloud) { activeReadBookInputHandler = controller + activeReadBookRoute = route MainActivity.hasActiveReadBookRoute = true controller.onClose = { onNavigateBack() } controller.onStartContentLoadFinish = { @@ -335,6 +336,9 @@ fun MainActivity.mainEntryProvider( if (activeReadBookInputHandler === controller) { activeReadBookInputHandler = null } + if (activeReadBookRoute == route) { + activeReadBookRoute = null + } MainActivity.hasActiveReadBookRoute = false controller.clearTts() } @@ -354,6 +358,7 @@ fun MainActivity.mainEntryProvider( entry { route -> val viewModel = koinViewModel( + key = route.bookUrl, parameters = { parametersOf(route) } ) SearchContentScreen( diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2364dff3a..073d0ae09 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -352,6 +352,7 @@ 下边距 左边距 右边距 + 上: %1$d 下: %2$d 左: %3$d 右: %4$d 校验书源 校验所选 %1$s 进度 %2$d/%3$d diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index a362ecf2a..663a98f39 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -299,6 +299,7 @@ 下邊距 左邊距 右邊距 + 上: %1$d 下: %2$d 左: %3$d 右: %4$d 校驗書源 校驗所選 %1$s 進度 %2$d/%3$d diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index dacc5b224..13fa803b2 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -301,6 +301,7 @@ 下邊距 左邊距 右邊距 + 上: %1$d 下: %2$d 左: %3$d 右: %4$d 校驗書源 校驗所選 %1$s 進度 %2$d/%3$d diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb0394a28..bd7822aa7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -373,6 +373,7 @@ Bottom Left Right + Top: %1$d Bottom: %2$d Left: %3$d Right: %4$d Check book sources Check the selected source %1$s Progress %2$d/%3$d