Merge branch 'main' into fix/theme-switch
This commit is contained in:
@@ -82,10 +82,6 @@ object PreferKey {
|
|||||||
const val exportNoChapterName = "exportNoChapterName"
|
const val exportNoChapterName = "exportNoChapterName"
|
||||||
const val exportType = "exportType"
|
const val exportType = "exportType"
|
||||||
const val exportPictureFile = "exportPictureFile"
|
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 chineseConverterType = "chineseConverterType"
|
||||||
const val launcherIcon = "launcherIcon"
|
const val launcherIcon = "launcherIcon"
|
||||||
const val textSelectAble = "selectText"
|
const val textSelectAble = "selectText"
|
||||||
|
|||||||
@@ -24,4 +24,11 @@ object LocalPreferencesKeys {
|
|||||||
val PASSWORD = stringPreferencesKey("password")
|
val PASSWORD = stringPreferencesKey("password")
|
||||||
val PRIVACY_POLICY_OK = booleanPreferencesKey("privacy_policy_ok")
|
val PRIVACY_POLICY_OK = booleanPreferencesKey("privacy_policy_ok")
|
||||||
val PERMISSION_CHECKED = booleanPreferencesKey("permission_checked")
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ class SearchContentRepository {
|
|||||||
matches.forEachIndexed { index, match ->
|
matches.forEachIndexed { index, match ->
|
||||||
val construct = getResultAndQueryIndex(mContent, match.position, match.length)
|
val construct = getResultAndQueryIndex(mContent, match.position, match.length)
|
||||||
val result = SearchResult(
|
val result = SearchResult(
|
||||||
|
bookUrl = book.bookUrl,
|
||||||
resultCountWithinChapter = index,
|
resultCountWithinChapter = index,
|
||||||
resultText = construct.second,
|
resultText = construct.second,
|
||||||
chapterTitle = chapter.title,
|
chapterTitle = chapter.title,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package io.legado.app.domain.model
|
package io.legado.app.domain.model
|
||||||
|
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import io.legado.app.utils.fromJsonObject
|
|
||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
data class BookSearchScope(val raw: String) {
|
data class BookSearchScope(val raw: String) {
|
||||||
|
|
||||||
@@ -39,11 +39,13 @@ data class BookSearchScope(val raw: String) {
|
|||||||
get() = groups.isEmpty() && sources.isEmpty()
|
get() = groups.isEmpty() && sources.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class ScopeSourceItem(
|
data class ScopeSourceItem(
|
||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
private data class SerializedSearchScope(
|
private data class SerializedSearchScope(
|
||||||
val type: String = "",
|
val type: String = "",
|
||||||
val groups: List<String> = emptyList(),
|
val groups: List<String> = emptyList(),
|
||||||
@@ -57,7 +59,9 @@ data class BookSearchScope(val raw: String) {
|
|||||||
return if (selected.isEmpty()) {
|
return if (selected.isEmpty()) {
|
||||||
""
|
""
|
||||||
} else {
|
} 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()) {
|
return if (selected.isEmpty()) {
|
||||||
""
|
""
|
||||||
} else {
|
} 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()
|
val json = raw.trim()
|
||||||
if (!json.startsWith("{") || !json.endsWith("}")) return null
|
if (!json.startsWith("{") || !json.endsWith("}")) return null
|
||||||
|
|
||||||
return scopeGson.fromJsonObject<SerializedSearchScope>(json).getOrNull()?.let { scope ->
|
return runCatching {
|
||||||
|
scopeJson.decodeFromString<SerializedSearchScope>(json)
|
||||||
|
}.getOrNull()?.let { scope ->
|
||||||
when (scope.type) {
|
when (scope.type) {
|
||||||
TYPE_SOURCE -> ParsedSearchScope(
|
TYPE_SOURCE -> ParsedSearchScope(
|
||||||
sources = scope.sources.filter { it.url.isNotBlank() }
|
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_GROUP = "group"
|
||||||
private const val TYPE_SOURCE = "source"
|
private const val TYPE_SOURCE = "source"
|
||||||
private val scopeGson = GsonBuilder().disableHtmlEscaping().create()
|
private val scopeJson = Json {
|
||||||
|
encodeDefaults = false
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import io.legado.app.help.book.BookHelp
|
|||||||
import io.legado.app.help.book.ContentProcessor
|
import io.legado.app.help.book.ContentProcessor
|
||||||
import io.legado.app.help.book.primaryStr
|
import io.legado.app.help.book.primaryStr
|
||||||
import io.legado.app.help.book.releaseHtmlData
|
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.help.source.SourceHelp
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import io.legado.app.ui.book.changesource.ObservableSourceConfig
|
import io.legado.app.ui.book.changesource.ObservableSourceConfig
|
||||||
@@ -135,10 +135,10 @@ class ChangeSourceSearchUseCase(
|
|||||||
fromReadBookActivity: Boolean,
|
fromReadBookActivity: Boolean,
|
||||||
contentProcessor: ContentProcessor,
|
contentProcessor: ContentProcessor,
|
||||||
): List<SearchBook> {
|
): List<SearchBook> {
|
||||||
val checkAuthor = AppConfig.changeSourceCheckAuthor
|
val checkAuthor = ChangeSourceConfig.checkAuthor
|
||||||
val loadInfo = AppConfig.changeSourceLoadInfo
|
val loadInfo = ChangeSourceConfig.loadInfo
|
||||||
val loadToc = AppConfig.changeSourceLoadToc
|
val loadToc = ChangeSourceConfig.loadToc
|
||||||
val loadWordCount = AppConfig.changeSourceLoadWordCount
|
val loadWordCount = ChangeSourceConfig.loadWordCount
|
||||||
|
|
||||||
val resultBooks = WebBook.searchBookAwait(
|
val resultBooks = WebBook.searchBookAwait(
|
||||||
source, name,
|
source, name,
|
||||||
|
|||||||
@@ -465,12 +465,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
appCtx.putPrefBoolean(PreferKey.parallelExportBook, value)
|
appCtx.putPrefBoolean(PreferKey.parallelExportBook, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var changeSourceCheckAuthor: Boolean
|
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor)
|
|
||||||
set(value) {
|
|
||||||
appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ttsEngine: String?
|
var ttsEngine: String?
|
||||||
get() = io.legado.app.ui.config.readConfig.ReadConfig.ttsEngine
|
get() = io.legado.app.ui.config.readConfig.ReadConfig.ttsEngine
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -504,24 +498,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
val autoChangeSource: Boolean
|
val autoChangeSource: Boolean
|
||||||
get() = ReadConfig.autoChangeSource
|
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
|
var openBookInfoByClickTitle: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.openBookInfoByClickTitle, true)
|
get() = appCtx.getPrefBoolean(PreferKey.openBookInfoByClickTitle, true)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import io.legado.app.base.adapter.DiffRecyclerAdapter
|
|||||||
import io.legado.app.base.adapter.ItemViewHolder
|
import io.legado.app.base.adapter.ItemViewHolder
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.databinding.ItemChangeSourceBinding
|
import io.legado.app.databinding.ItemChangeSourceBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
import io.legado.app.utils.invisible
|
import io.legado.app.utils.invisible
|
||||||
@@ -91,13 +90,13 @@ class ChangeBookSourceAdapter(
|
|||||||
binding.ivGood.setImageResource(R.drawable.ic_praise)
|
binding.ivGood.setImageResource(R.drawable.ic_praise)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AppConfig.changeSourceLoadWordCount && !item.chapterWordCountText.isNullOrBlank()) {
|
if (ChangeSourceConfig.loadWordCount && !item.chapterWordCountText.isNullOrBlank()) {
|
||||||
tvCurrentChapterWordCount.visible()
|
tvCurrentChapterWordCount.visible()
|
||||||
} else {
|
} else {
|
||||||
tvCurrentChapterWordCount.gone()
|
tvCurrentChapterWordCount.gone()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AppConfig.changeSourceLoadWordCount && item.respondTime >= 0) {
|
if (ChangeSourceConfig.loadWordCount && item.respondTime >= 0) {
|
||||||
tvRespondTime.visible()
|
tvRespondTime.visible()
|
||||||
} else {
|
} else {
|
||||||
tvRespondTime.gone()
|
tvRespondTime.gone()
|
||||||
|
|||||||
+2
@@ -227,11 +227,13 @@ class ChangeBookSourceComposeViewModel(
|
|||||||
fun onLoadInfoChange(enabled: Boolean) {
|
fun onLoadInfoChange(enabled: Boolean) {
|
||||||
if (ChangeSourceConfig.loadInfo == enabled) return
|
if (ChangeSourceConfig.loadInfo == enabled) return
|
||||||
ChangeSourceConfig.loadInfo = enabled
|
ChangeSourceConfig.loadInfo = enabled
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLoadTocChange(enabled: Boolean) {
|
fun onLoadTocChange(enabled: Boolean) {
|
||||||
if (ChangeSourceConfig.loadToc == enabled) return
|
if (ChangeSourceConfig.loadToc == enabled) return
|
||||||
ChangeSourceConfig.loadToc = enabled
|
ChangeSourceConfig.loadToc = enabled
|
||||||
|
refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLoadWordCountChange(enabled: Boolean) {
|
fun onLoadWordCountChange(enabled: Boolean) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import io.legado.app.data.entities.BookChapter
|
|||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.databinding.DialogBookChangeSourceBinding
|
import io.legado.app.databinding.DialogBookChangeSourceBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.ui.book.search.SearchScope
|
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.menu.applyTint(requireContext())
|
||||||
binding.toolBar.setOnMenuItemClickListener(this)
|
binding.toolBar.setOnMenuItemClickListener(this)
|
||||||
binding.toolBar.menu.findItem(R.id.menu_check_author)
|
binding.toolBar.menu.findItem(R.id.menu_check_author)
|
||||||
?.isChecked = AppConfig.changeSourceCheckAuthor
|
?.isChecked = ChangeSourceConfig.checkAuthor
|
||||||
binding.toolBar.menu.findItem(R.id.menu_load_info)
|
binding.toolBar.menu.findItem(R.id.menu_load_info)
|
||||||
?.isChecked = AppConfig.changeSourceLoadInfo
|
?.isChecked = ChangeSourceConfig.loadInfo
|
||||||
binding.toolBar.menu.findItem(R.id.menu_load_toc)
|
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)
|
binding.toolBar.menu.findItem(R.id.menu_load_word_count)
|
||||||
?.isChecked = AppConfig.changeSourceLoadWordCount
|
?.isChecked = ChangeSourceConfig.loadWordCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
private fun initRecyclerView() {
|
||||||
@@ -316,23 +315,25 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
|
|||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
||||||
when (item?.itemId) {
|
when (item?.itemId) {
|
||||||
R.id.menu_check_author -> {
|
R.id.menu_check_author -> {
|
||||||
AppConfig.changeSourceCheckAuthor = !item.isChecked
|
ChangeSourceConfig.checkAuthor = !item.isChecked
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
viewModel.refresh()
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_load_info -> {
|
R.id.menu_load_info -> {
|
||||||
AppConfig.changeSourceLoadInfo = !item.isChecked
|
ChangeSourceConfig.loadInfo = !item.isChecked
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_load_toc -> {
|
R.id.menu_load_toc -> {
|
||||||
AppConfig.changeSourceLoadToc = !item.isChecked
|
ChangeSourceConfig.loadToc = !item.isChecked
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
|
viewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_load_word_count -> {
|
R.id.menu_load_word_count -> {
|
||||||
AppConfig.changeSourceLoadWordCount = !item.isChecked
|
ChangeSourceConfig.loadWordCount = !item.isChecked
|
||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
viewModel.onLoadWordCountChecked(item.isChecked)
|
viewModel.onLoadWordCountChecked(item.isChecked)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import io.legado.app.help.book.BookHelp
|
|||||||
import io.legado.app.help.book.ContentProcessor
|
import io.legado.app.help.book.ContentProcessor
|
||||||
import io.legado.app.help.book.primaryStr
|
import io.legado.app.help.book.primaryStr
|
||||||
import io.legado.app.help.book.releaseHtmlData
|
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.config.SourceConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.help.source.SourceHelp
|
import io.legado.app.help.source.SourceHelp
|
||||||
@@ -137,7 +136,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
}
|
}
|
||||||
}.map {
|
}.map {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val comparator = if (AppConfig.changeSourceLoadWordCount) {
|
val comparator = if (ChangeSourceConfig.loadWordCount) {
|
||||||
wordCountComparator
|
wordCountComparator
|
||||||
} else {
|
} else {
|
||||||
defaultComparator
|
defaultComparator
|
||||||
@@ -269,10 +268,10 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun search(source: BookSource) {
|
private suspend fun search(source: BookSource) {
|
||||||
val checkAuthor = AppConfig.changeSourceCheckAuthor
|
val checkAuthor = ChangeSourceConfig.checkAuthor
|
||||||
val loadInfo = AppConfig.changeSourceLoadInfo
|
val loadInfo = ChangeSourceConfig.loadInfo
|
||||||
val loadToc = AppConfig.changeSourceLoadToc
|
val loadToc = ChangeSourceConfig.loadToc
|
||||||
val loadWordCount = AppConfig.changeSourceLoadWordCount
|
val loadWordCount = ChangeSourceConfig.loadWordCount
|
||||||
val resultBooks = WebBook.searchBookAwait(
|
val resultBooks = WebBook.searchBookAwait(
|
||||||
source, name,
|
source, name,
|
||||||
filter = { fName, fAuthor, _ ->
|
filter = { fName, fAuthor, _ ->
|
||||||
@@ -295,7 +294,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
if (book.tocUrl.isEmpty()) {
|
if (book.tocUrl.isEmpty()) {
|
||||||
WebBook.getBookInfoAwait(source, book)
|
WebBook.getBookInfoAwait(source, book)
|
||||||
}
|
}
|
||||||
if (AppConfig.changeSourceLoadToc || AppConfig.changeSourceLoadWordCount) {
|
if (ChangeSourceConfig.loadToc || ChangeSourceConfig.loadWordCount) {
|
||||||
loadBookToc(source, book)
|
loadBookToc(source, book)
|
||||||
} else {
|
} else {
|
||||||
//从详情页里获取最新章节
|
//从详情页里获取最新章节
|
||||||
@@ -315,7 +314,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
}
|
}
|
||||||
bookMap[book.primaryStr()] = book
|
bookMap[book.primaryStr()] = book
|
||||||
book.releaseHtmlData()
|
book.releaseHtmlData()
|
||||||
if (AppConfig.changeSourceLoadWordCount) {
|
if (ChangeSourceConfig.loadWordCount) {
|
||||||
loadBookWordCount(source, book, chapters)
|
loadBookWordCount(source, book, chapters)
|
||||||
} else {
|
} else {
|
||||||
val searchBook = book.toSearchBook()
|
val searchBook = book.toSearchBook()
|
||||||
@@ -418,13 +417,13 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
|
|||||||
else -> searchScope.displayNames.firstOrNull() ?: ""
|
else -> searchScope.displayNames.firstOrNull() ?: ""
|
||||||
}
|
}
|
||||||
return if (screenKey.isEmpty()) {
|
return if (screenKey.isEmpty()) {
|
||||||
if (AppConfig.changeSourceCheckAuthor) {
|
if (ChangeSourceConfig.checkAuthor) {
|
||||||
appDb.searchBookDao.changeSourceByGroup(name, author, group)
|
appDb.searchBookDao.changeSourceByGroup(name, author, group)
|
||||||
} else {
|
} else {
|
||||||
appDb.searchBookDao.changeSourceByGroup(name, "", group)
|
appDb.searchBookDao.changeSourceByGroup(name, "", group)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (AppConfig.changeSourceCheckAuthor) {
|
if (ChangeSourceConfig.checkAuthor) {
|
||||||
appDb.searchBookDao.changeSourceSearch(name, author, screenKey, group)
|
appDb.searchBookDao.changeSourceSearch(name, author, screenKey, group)
|
||||||
} else {
|
} else {
|
||||||
appDb.searchBookDao.changeSourceSearch(name, "", screenKey, group)
|
appDb.searchBookDao.changeSourceSearch(name, "", screenKey, group)
|
||||||
|
|||||||
@@ -133,11 +133,13 @@ class ChangeChapterSourceViewModel(
|
|||||||
is ChangeChapterSourceIntent.SetLoadInfo -> {
|
is ChangeChapterSourceIntent.SetLoadInfo -> {
|
||||||
ChangeSourceConfig.loadInfo = intent.enabled
|
ChangeSourceConfig.loadInfo = intent.enabled
|
||||||
_uiState.update { it.copy(loadInfo = intent.enabled) }
|
_uiState.update { it.copy(loadInfo = intent.enabled) }
|
||||||
|
refreshResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChangeChapterSourceIntent.SetLoadToc -> {
|
is ChangeChapterSourceIntent.SetLoadToc -> {
|
||||||
ChangeSourceConfig.loadToc = intent.enabled
|
ChangeSourceConfig.loadToc = intent.enabled
|
||||||
_uiState.update { it.copy(loadToc = intent.enabled) }
|
_uiState.update { it.copy(loadToc = intent.enabled) }
|
||||||
|
refreshResults()
|
||||||
}
|
}
|
||||||
|
|
||||||
is ChangeChapterSourceIntent.SetLoadWordCount -> {
|
is ChangeChapterSourceIntent.SetLoadWordCount -> {
|
||||||
|
|||||||
@@ -1,35 +1,83 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
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.domain.usecase.ChangeSourceMigrationOptions
|
||||||
import io.legado.app.ui.config.prefDelegate
|
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 {
|
object ChangeSourceConfig {
|
||||||
|
|
||||||
var searchScope by prefDelegate(
|
private val repo = LocalPreferencesRepository(appCtx)
|
||||||
key = "changeSourceSearchScope",
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
defaultValue = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
var checkAuthor by prefDelegate(
|
private val _searchScope = mutableStateOf("")
|
||||||
key = PreferKey.changeSourceCheckAuthor,
|
var searchScope: String
|
||||||
defaultValue = false
|
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(
|
private val _checkAuthor = mutableStateOf(false)
|
||||||
key = PreferKey.changeSourceLoadInfo,
|
var checkAuthor: Boolean
|
||||||
defaultValue = false
|
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(
|
private val _loadInfo = mutableStateOf(false)
|
||||||
key = PreferKey.changeSourceLoadToc,
|
var loadInfo: Boolean
|
||||||
defaultValue = false
|
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(
|
private val _loadToc = mutableStateOf(false)
|
||||||
key = PreferKey.changeSourceLoadWordCount,
|
var loadToc: Boolean
|
||||||
defaultValue = false
|
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(
|
var migrateChapters by prefDelegate(
|
||||||
key = "migrateChapters",
|
key = "migrateChapters",
|
||||||
@@ -64,6 +112,44 @@ object ChangeSourceConfig {
|
|||||||
defaultValue = false
|
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 {
|
fun getMigrationOptions(): ChangeSourceMigrationOptions {
|
||||||
return ChangeSourceMigrationOptions(
|
return ChangeSourceMigrationOptions(
|
||||||
migrateChapters = migrateChapters,
|
migrateChapters = migrateChapters,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ sealed interface ReadBookMenuRoute {
|
|||||||
data object ReadAloud : ReadBookMenuRoute
|
data object ReadAloud : ReadBookMenuRoute
|
||||||
data object AutoRead : ReadBookMenuRoute
|
data object AutoRead : ReadBookMenuRoute
|
||||||
data object PaddingConfig : ReadBookMenuRoute
|
data object PaddingConfig : ReadBookMenuRoute
|
||||||
|
data object HeaderFooterConfig : ReadBookMenuRoute
|
||||||
data class Bookmark(val bookmark: io.legado.app.data.entities.Bookmark) : ReadBookMenuRoute
|
data class Bookmark(val bookmark: io.legado.app.data.entities.Bookmark) : ReadBookMenuRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -717,7 +717,7 @@ class ReadBookController(
|
|||||||
// Chapter not loaded — open it, then mark in the success callback
|
// Chapter not loaded — open it, then mark in the success callback
|
||||||
ReadBook.openChapter(
|
ReadBook.openChapter(
|
||||||
effect.chapterIndex,
|
effect.chapterIndex,
|
||||||
effect.result.queryIndexInChapter
|
0
|
||||||
) {
|
) {
|
||||||
val tc = ReadBook.curTextChapter ?: return@openChapter
|
val tc = ReadBook.curTextChapter ?: return@openChapter
|
||||||
val query = effect.result.query
|
val query = effect.result.query
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ import io.legado.app.help.config.ReadStyleResolver
|
|||||||
import io.legado.app.ui.animation.DampedDragAnimation
|
import io.legado.app.ui.animation.DampedDragAnimation
|
||||||
import io.legado.app.ui.animation.InteractiveHighlight
|
import io.legado.app.ui.animation.InteractiveHighlight
|
||||||
import io.legado.app.ui.book.read.sheet.AutoReadContent
|
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.PaddingConfigContent
|
||||||
import io.legado.app.ui.book.read.sheet.ReadAloudContent
|
import io.legado.app.ui.book.read.sheet.ReadAloudContent
|
||||||
import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
|
import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
|
||||||
@@ -186,7 +187,7 @@ fun ReadBookMenuBar(
|
|||||||
} else {
|
} else {
|
||||||
ReadBookMenuContent.Route(currentRoute)
|
ReadBookMenuContent.Route(currentRoute)
|
||||||
}
|
}
|
||||||
val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig
|
val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig || currentRoute == ReadBookMenuRoute.HeaderFooterConfig
|
||||||
var readStylePage by remember { mutableIntStateOf(0) }
|
var readStylePage by remember { mutableIntStateOf(0) }
|
||||||
LaunchedEffect(currentRoute) {
|
LaunchedEffect(currentRoute) {
|
||||||
if (currentRoute != ReadBookMenuRoute.ReadStyle) {
|
if (currentRoute != ReadBookMenuRoute.ReadStyle) {
|
||||||
@@ -392,7 +393,7 @@ private fun ReadBookMenuSurface(
|
|||||||
is ReadBookMenuContent.Route -> contentTarget.route
|
is ReadBookMenuContent.Route -> contentTarget.route
|
||||||
}
|
}
|
||||||
val expanded = route != ReadBookMenuRoute.Main
|
val expanded = route != ReadBookMenuRoute.Main
|
||||||
val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig
|
val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig || route == ReadBookMenuRoute.HeaderFooterConfig
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val windowSize = LocalWindowInfo.current.containerSize
|
val windowSize = LocalWindowInfo.current.containerSize
|
||||||
var surfaceHeightPx by remember { mutableIntStateOf(0) }
|
var surfaceHeightPx by remember { mutableIntStateOf(0) }
|
||||||
@@ -400,18 +401,6 @@ private fun ReadBookMenuSurface(
|
|||||||
targetValue = if (dialogLikeRoute) 1f else 0f,
|
targetValue = if (dialogLikeRoute) 1f else 0f,
|
||||||
label = "ReadBookMenuMorph",
|
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) {
|
val maxHeight = with(density) {
|
||||||
windowSize.height.toDp() * 0.64f
|
windowSize.height.toDp() * 0.64f
|
||||||
}
|
}
|
||||||
@@ -518,8 +507,7 @@ private fun ReadBookMenuSurface(
|
|||||||
.onSizeChanged { surfaceHeightPx = it.height }
|
.onSizeChanged { surfaceHeightPx = it.height }
|
||||||
.offset {
|
.offset {
|
||||||
val dialogLiftPx = ((windowSize.height - surfaceHeightPx) / 2f) * morphProgress
|
val dialogLiftPx = ((windowSize.height - surfaceHeightPx) / 2f) * morphProgress
|
||||||
val liftPx = dialogLiftPx + headerFooterLift.toPx()
|
IntOffset(x = 0, y = -dialogLiftPx.roundToInt())
|
||||||
IntOffset(x = 0, y = -liftPx.roundToInt())
|
|
||||||
}
|
}
|
||||||
.then(
|
.then(
|
||||||
if (useLiquidGlass) {
|
if (useLiquidGlass) {
|
||||||
@@ -620,6 +608,9 @@ private fun ReadBookMenuSurface(
|
|||||||
onOpenPaddingConfig = {
|
onOpenPaddingConfig = {
|
||||||
onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
|
onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
|
||||||
},
|
},
|
||||||
|
onOpenHeaderFooterConfig = {
|
||||||
|
onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.HeaderFooterConfig))
|
||||||
|
},
|
||||||
onOpenMoreConfig = {
|
onOpenMoreConfig = {
|
||||||
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreConfig))
|
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 -> {
|
ReadBookMenuRoute.TextTitle -> {
|
||||||
ReadBookMenuRoutePage(
|
ReadBookMenuRoutePage(
|
||||||
title = stringResource(R.string.read_config_text_effects),
|
title = stringResource(R.string.read_config_text_effects),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import io.legado.app.R
|
|||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.ReadMenuBlurMode
|
import io.legado.app.constant.ReadMenuBlurMode
|
||||||
import io.legado.app.help.IntentHelp
|
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.info.BookInfoActivity
|
||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
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.ReadView
|
||||||
@@ -383,6 +384,10 @@ fun ReadBookRouteScreen(
|
|||||||
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
SearchContentResult.results.collect { result ->
|
SearchContentResult.results.collect { result ->
|
||||||
effectsReady.await()
|
effectsReady.await()
|
||||||
|
if (result.bookUrl != ReadBook.book?.bookUrl) {
|
||||||
|
SearchContentResult.resetReplayCache()
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
viewModel.onIntent(
|
viewModel.onIntent(
|
||||||
ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query)
|
ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2241,50 +2241,21 @@ class ReadBookViewModel(
|
|||||||
return arrayOf(-1, 0, 0, 0, 0, 0)
|
return arrayOf(-1, 0, 0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pageIndex = 0
|
val start = findSearchTextPoint(pages, contentPosition, preferPreviousLine = false)
|
||||||
var length = pages[pageIndex].text.length
|
?: return arrayOf(-1, 0, 0, 0, 0, 0)
|
||||||
while (length < contentPosition && pageIndex + 1 < pages.size) {
|
val end = findSearchTextPoint(
|
||||||
pageIndex += 1
|
pages,
|
||||||
length += pages[pageIndex].text.length
|
contentPosition + queryLength - 1,
|
||||||
}
|
preferPreviousLine = true
|
||||||
if (length < contentPosition) return arrayOf(-1, 0, 0, 0, 0, 0)
|
) ?: start
|
||||||
|
return arrayOf(
|
||||||
val currentPage = pages[pageIndex]
|
start.pageIndex,
|
||||||
val lines = currentPage.lines
|
start.lineIndex,
|
||||||
if (lines.isEmpty()) return arrayOf(-1, 0, 0, 0, 0, 0)
|
start.charIndex,
|
||||||
|
end.pageIndex - start.pageIndex,
|
||||||
var lineIndex = 0
|
end.lineIndex,
|
||||||
var currentLine = lines[lineIndex]
|
end.charIndex
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSearchResultMatch(
|
private fun findSearchResultMatch(
|
||||||
@@ -2293,6 +2264,26 @@ class ReadBookViewModel(
|
|||||||
query: String,
|
query: String,
|
||||||
): Pair<Int, Int>? {
|
): Pair<Int, Int>? {
|
||||||
if (query.isEmpty()) return null
|
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) {
|
if (searchResult.isRegex) {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
Regex(query).findAll(content)
|
Regex(query).findAll(content)
|
||||||
@@ -2311,6 +2302,43 @@ class ReadBookViewModel(
|
|||||||
return index.takeIf { it >= 0 }?.let { it to query.length }
|
return index.takeIf { it >= 0 }?.let { it to query.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun findSearchTextPoint(
|
||||||
|
pages: List<TextPage>,
|
||||||
|
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]
|
* Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult]
|
||||||
* so the Controller can navigate and highlight.
|
* 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 TOOL_BUTTON_KEY = "tool_buttons"
|
||||||
private const val DEFAULT_ENABLED_BUTTON_COUNT = 5
|
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 {
|
private fun Int.coerceSearchResultIndex(resultSize: Int): Int {
|
||||||
return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1)
|
return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
package io.legado.app.ui.book.read.sheet
|
package io.legado.app.ui.book.read.sheet
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.tween
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
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.material.icons.filled.TextFields
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -32,11 +42,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.help.config.ReadBookConfig
|
||||||
import io.legado.app.ui.book.read.ConfigUpdate
|
import io.legado.app.ui.book.read.ConfigUpdate
|
||||||
import io.legado.app.ui.book.read.ReadBookIntent
|
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.FontSelectSheet
|
||||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
|
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
|
||||||
@@ -106,6 +115,42 @@ internal fun HeaderFooterPage(
|
|||||||
var colorPickerInitial by remember { mutableIntStateOf(0) }
|
var colorPickerInitial by remember { mutableIntStateOf(0) }
|
||||||
var showFontSelect by remember { mutableStateOf(false) }
|
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 tipNames = remember { ReadBookConfig.tipNames }
|
||||||
val tipValues = remember { ReadBookConfig.tipValues }
|
val tipValues = remember { ReadBookConfig.tipValues }
|
||||||
|
|
||||||
@@ -153,7 +198,7 @@ internal fun HeaderFooterPage(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
page = index,
|
page = index,
|
||||||
animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -181,7 +226,7 @@ internal fun HeaderFooterPage(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(headerScrollState),
|
||||||
) {
|
) {
|
||||||
TinySwitchSettingItem(
|
TinySwitchSettingItem(
|
||||||
title = stringResource(R.string.showLine),
|
title = stringResource(R.string.showLine),
|
||||||
@@ -252,6 +297,56 @@ internal fun HeaderFooterPage(
|
|||||||
showColorPicker = true
|
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
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(footerScrollState),
|
||||||
) {
|
) {
|
||||||
TinySwitchSettingItem(
|
TinySwitchSettingItem(
|
||||||
title = stringResource(R.string.showLine),
|
title = stringResource(R.string.showLine),
|
||||||
@@ -332,6 +427,56 @@ internal fun HeaderFooterPage(
|
|||||||
showColorPicker = true
|
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())))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,11 @@ package io.legado.app.ui.book.read.sheet
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
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.book.read.ReadBookIntent
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
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.settingItem.TinySliderSettingItem
|
||||||
import io.legado.app.ui.widget.components.tabRow.CardTabRow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PaddingConfigSheet(
|
fun PaddingConfigSheet(
|
||||||
@@ -58,122 +49,35 @@ fun PaddingConfigContent(
|
|||||||
var paddingBottom by remember { mutableFloatStateOf(ReadBookConfig.paddingBottom.toFloat()) }
|
var paddingBottom by remember { mutableFloatStateOf(ReadBookConfig.paddingBottom.toFloat()) }
|
||||||
var paddingLeft by remember { mutableFloatStateOf(ReadBookConfig.paddingLeft.toFloat()) }
|
var paddingLeft by remember { mutableFloatStateOf(ReadBookConfig.paddingLeft.toFloat()) }
|
||||||
var paddingRight by remember { mutableFloatStateOf(ReadBookConfig.paddingRight.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(
|
Column(
|
||||||
modifier = modifier.fillMaxWidth(),
|
modifier = modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
CardTabRow(
|
PaddingSliders(
|
||||||
tabTitles = tabTitles,
|
top = paddingTop, bottom = paddingBottom,
|
||||||
selectedTabIndex = selectedTab,
|
left = paddingLeft, right = paddingRight,
|
||||||
onTabSelected = { index ->
|
onTopChange = {
|
||||||
selectedTab = index
|
paddingTop = it
|
||||||
scope.launch { pagerState.animateScrollToPage(index) }
|
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
|
@Composable
|
||||||
private fun PaddingSliders(
|
internal fun PaddingSliders(
|
||||||
top: Float, bottom: Float, left: Float, right: Float,
|
top: Float, bottom: Float, left: Float, right: Float,
|
||||||
onTopChange: (Float) -> Unit, onBottomChange: (Float) -> Unit,
|
onTopChange: (Float) -> Unit, onBottomChange: (Float) -> Unit,
|
||||||
onLeftChange: (Float) -> Unit, onRightChange: (Float) -> Unit,
|
onLeftChange: (Float) -> Unit, onRightChange: (Float) -> Unit,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun ReadStyleContent(
|
fun ReadStyleContent(
|
||||||
onOpenPaddingConfig: () -> Unit,
|
onOpenPaddingConfig: () -> Unit,
|
||||||
|
onOpenHeaderFooterConfig: () -> Unit,
|
||||||
onOpenMoreConfig: () -> Unit,
|
onOpenMoreConfig: () -> Unit,
|
||||||
onOpenBgTextConfig: (Int) -> Unit,
|
onOpenBgTextConfig: (Int) -> Unit,
|
||||||
onOpenTextTitle: () -> Unit,
|
onOpenTextTitle: () -> Unit,
|
||||||
@@ -51,7 +52,7 @@ fun ReadStyleContent(
|
|||||||
styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
|
styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val pagerState = rememberPagerState(pageCount = { 3 })
|
val pagerState = rememberPagerState(pageCount = { 2 })
|
||||||
var currentPage by remember { mutableIntStateOf(0) }
|
var currentPage by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
val pageHeights = remember { mutableStateMapOf<Int, Int>() }
|
val pageHeights = remember { mutableStateMapOf<Int, Int>() }
|
||||||
@@ -105,10 +106,6 @@ fun ReadStyleContent(
|
|||||||
bottomBarButtons = bottomBarButtons,
|
bottomBarButtons = bottomBarButtons,
|
||||||
onIntent = onIntent,
|
onIntent = onIntent,
|
||||||
)
|
)
|
||||||
|
|
||||||
2 -> HeaderFooterPage(
|
|
||||||
onIntent = onIntent,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,15 +120,17 @@ fun ReadStyleContent(
|
|||||||
tabTitles = tabTitles,
|
tabTitles = tabTitles,
|
||||||
selectedTabIndex = currentPage,
|
selectedTabIndex = currentPage,
|
||||||
onTabSelected = { index ->
|
onTabSelected = { index ->
|
||||||
if (index < 3) {
|
when (index) {
|
||||||
scope.launch {
|
0, 1 -> {
|
||||||
pagerState.animateScrollToPage(
|
scope.launch {
|
||||||
page = index,
|
pagerState.animateScrollToPage(
|
||||||
animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
page = index,
|
||||||
)
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
2 -> onOpenHeaderFooterConfig()
|
||||||
onOpenMoreConfig()
|
3 -> onOpenMoreConfig()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ internal fun SystemMenuPage(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
pagerState.animateScrollToPage(
|
pagerState.animateScrollToPage(
|
||||||
page = index,
|
page = index,
|
||||||
animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)
|
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.legado.app.R
|
|||||||
import io.legado.app.data.entities.BookSourcePart
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
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.button.series.MediumPlainButton
|
||||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
import io.legado.app.ui.widget.components.icon.AppIcons
|
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||||
@@ -46,9 +47,20 @@ fun ScopeSelectSheet(
|
|||||||
isSourceScope: Boolean = false,
|
isSourceScope: Boolean = false,
|
||||||
title: String = stringResource(R.string.search_select_group),
|
title: String = stringResource(R.string.search_select_group),
|
||||||
onConfirm: (() -> Unit)? = null,
|
onConfirm: (() -> Unit)? = null,
|
||||||
|
onApplyScope: ((ScopeSelection) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
var scopeSheetTab by rememberSaveable(show) { mutableIntStateOf(if (isSourceScope) 1 else 0) }
|
var scopeSheetTab by rememberSaveable(show) { mutableIntStateOf(if (isSourceScope) 1 else 0) }
|
||||||
var filterText by rememberSaveable(show) { mutableStateOf("") }
|
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) {
|
val filteredGroups = remember(groups, filterText) {
|
||||||
if (filterText.isBlank()) groups else groups.filter { it.contains(filterText, ignoreCase = true) }
|
if (filterText.isBlank()) groups else groups.filter { it.contains(filterText, ignoreCase = true) }
|
||||||
@@ -87,11 +99,18 @@ fun ScopeSelectSheet(
|
|||||||
|
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = stringResource(R.string.all_source),
|
title = stringResource(R.string.all_source),
|
||||||
isSelected = isAll,
|
isSelected = currentIsAll,
|
||||||
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
inSelectionMode = true,
|
inSelectionMode = true,
|
||||||
onToggleSelection = {
|
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)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(filteredGroups, key = { it }) { groupName ->
|
items(filteredGroups, key = { it }) { groupName ->
|
||||||
val selected = !isSourceScope && selectedGroups.contains(groupName)
|
val selected = !currentIsSourceScope && currentGroups.contains(groupName)
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = groupName,
|
title = groupName,
|
||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
containerColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.6f),
|
||||||
inSelectionMode = true,
|
inSelectionMode = true,
|
||||||
onToggleSelection = {
|
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)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(filteredSources, key = { it.bookSourceUrl }) { source ->
|
items(filteredSources, key = { it.bookSourceUrl }) { source ->
|
||||||
val selected = selectedSources.contains(source.bookSourceUrl)
|
val selected = currentSourceUrls.contains(source.bookSourceUrl)
|
||||||
SelectionItemCard(
|
SelectionItemCard(
|
||||||
title = source.bookSourceName,
|
title = source.bookSourceName,
|
||||||
subtitle = source.bookSourceGroup?.takeIf { group -> group.isNotBlank() },
|
subtitle = source.bookSourceGroup?.takeIf { group -> group.isNotBlank() },
|
||||||
@@ -150,7 +182,20 @@ fun ScopeSelectSheet(
|
|||||||
isSelected = selected,
|
isSelected = selected,
|
||||||
inSelectionMode = true,
|
inSelectionMode = true,
|
||||||
onToggleSelection = {
|
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))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class ScopeSelection(
|
||||||
|
val groupNames: List<String>,
|
||||||
|
val sources: List<BookSourcePart>,
|
||||||
|
val isSourceScope: Boolean,
|
||||||
|
)
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ sealed interface SearchIntent {
|
|||||||
data class ToggleSourceType(val type: Int) : SearchIntent
|
data class ToggleSourceType(val type: Int) : SearchIntent
|
||||||
data object ClearAllSourceTypes : SearchIntent
|
data object ClearAllSourceTypes : SearchIntent
|
||||||
data object SelectAllScope : SearchIntent
|
data object SelectAllScope : SearchIntent
|
||||||
|
data class ApplyScopeSelection(
|
||||||
|
val groupNames: List<String>,
|
||||||
|
val sources: List<BookSourcePart>,
|
||||||
|
val isSourceScope: Boolean,
|
||||||
|
) : SearchIntent
|
||||||
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
data class ToggleScopeGroup(val groupName: String) : SearchIntent
|
||||||
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
data class ToggleScopeSource(val source: BookSourcePart) : SearchIntent
|
||||||
data class RemoveScopeItem(val scopeName: String) : SearchIntent
|
data class RemoveScopeItem(val scopeName: String) : SearchIntent
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ data class SearchScope(private var scope: String) {
|
|||||||
if (scope.isEmpty()) {
|
if (scope.isEmpty()) {
|
||||||
return appCtx.getString(R.string.all_source)
|
return appCtx.getString(R.string.all_source)
|
||||||
}
|
}
|
||||||
return scope
|
return parsedScope().groupNames.joinToString(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ fun SearchScreen(
|
|||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
var queryInput by rememberSaveable { mutableStateOf(state.query) }
|
||||||
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
var ignoreNextDebouncedQuery by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
var keepResultsPinnedToTop by rememberSaveable { mutableStateOf(true) }
|
||||||
val showSuggestionPanel = state.showSuggestions
|
val showSuggestionPanel = state.showSuggestions
|
||||||
val latestQuery by rememberUpdatedState(state.query)
|
val latestQuery by rememberUpdatedState(state.query)
|
||||||
val scrollBehavior = if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
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 submitSearch: (String) -> Unit = { rawQuery ->
|
||||||
val normalized = rawQuery.trim()
|
val normalized = rawQuery.trim()
|
||||||
if (normalized.isNotBlank()) {
|
if (normalized.isNotBlank()) {
|
||||||
@@ -611,6 +643,15 @@ fun SearchScreen(
|
|||||||
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
onToggleSource = { viewModel.onIntent(SearchIntent.ToggleScopeSource(it)) },
|
||||||
isSourceScope = state.isSourceScope,
|
isSourceScope = state.isSourceScope,
|
||||||
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
|
onConfirm = { viewModel.onIntent(SearchIntent.OpenSourceManage) },
|
||||||
|
onApplyScope = { selection ->
|
||||||
|
viewModel.onIntent(
|
||||||
|
SearchIntent.ApplyScopeSelection(
|
||||||
|
groupNames = selection.groupNames,
|
||||||
|
sources = selection.sources,
|
||||||
|
isSourceScope = selection.isSourceScope,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ class SearchViewModel(
|
|||||||
syncScopeState(restartSearch = true, oldScope = oldScope)
|
syncScopeState(restartSearch = true, oldScope = oldScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is SearchIntent.ApplyScopeSelection -> applyScopeSelection(intent)
|
||||||
is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
|
is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName)
|
||||||
is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
|
is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source)
|
||||||
is SearchIntent.RemoveScopeItem -> {
|
is SearchIntent.RemoveScopeItem -> {
|
||||||
@@ -221,6 +222,7 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is SearchIntent.SetMatchMode -> {
|
is SearchIntent.SetMatchMode -> {
|
||||||
|
_uiState.update { it.copy(matchMode = intent.mode) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
localPreferencesRepository.updatePreference(
|
localPreferencesRepository.updatePreference(
|
||||||
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
|
LocalPreferencesKeys.MATCH_MODE, intent.mode.value
|
||||||
@@ -427,7 +429,6 @@ class SearchViewModel(
|
|||||||
it.copy(
|
it.copy(
|
||||||
query = query,
|
query = query,
|
||||||
showSuggestions = showSuggestions,
|
showSuggestions = showSuggestions,
|
||||||
isManualStop = false,
|
|
||||||
emptyScopeAction = null,
|
emptyScopeAction = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -657,6 +658,7 @@ class SearchViewModel(
|
|||||||
_uiState.update { it.copy(emptyScopeAction = null) }
|
_uiState.update { it.copy(emptyScopeAction = null) }
|
||||||
|
|
||||||
if (action.wasMatchMode == MatchMode.EXACT) {
|
if (action.wasMatchMode == MatchMode.EXACT) {
|
||||||
|
_uiState.update { it.copy(matchMode = MatchMode.DEFAULT) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
localPreferencesRepository.updatePreference(
|
localPreferencesRepository.updatePreference(
|
||||||
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
|
LocalPreferencesKeys.MATCH_MODE, MatchMode.DEFAULT.value
|
||||||
@@ -672,12 +674,29 @@ class SearchViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun restartCommittedSearchIfNeeded() {
|
private fun restartCommittedSearchIfNeeded() {
|
||||||
val committed = _uiState.value.committedQuery
|
val state = _uiState.value
|
||||||
if (committed.isNotBlank()) {
|
val committed = state.committedQuery
|
||||||
|
if (
|
||||||
|
committed.isNotBlank() &&
|
||||||
|
state.query.trim() == committed &&
|
||||||
|
!state.showSuggestions &&
|
||||||
|
!state.isManualStop
|
||||||
|
) {
|
||||||
submitSearch(committed)
|
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(
|
private fun syncScopeState(
|
||||||
restartSearch: Boolean = false,
|
restartSearch: Boolean = false,
|
||||||
oldScope: String? = null,
|
oldScope: String? = null,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.book.searchContent
|
package io.legado.app.ui.book.searchContent
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow
|
|||||||
object SearchContentResult {
|
object SearchContentResult {
|
||||||
|
|
||||||
data class Result(
|
data class Result(
|
||||||
|
val bookUrl: String,
|
||||||
val searchResults: List<SearchResult>,
|
val searchResults: List<SearchResult>,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val query: String,
|
val query: String,
|
||||||
@@ -25,6 +27,7 @@ object SearchContentResult {
|
|||||||
_results.tryEmit(result)
|
_results.tryEmit(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun resetReplayCache() {
|
fun resetReplayCache() {
|
||||||
_results.resetReplayCache()
|
_results.resetReplayCache()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ class SearchContentViewModel(
|
|||||||
if (index < 0) return false
|
if (index < 0) return false
|
||||||
SearchContentResult.emitResult(
|
SearchContentResult.emitResult(
|
||||||
SearchContentResult.Result(
|
SearchContentResult.Result(
|
||||||
|
bookUrl = bookUrl,
|
||||||
searchResults = results,
|
searchResults = results,
|
||||||
index = index,
|
index = index,
|
||||||
query = searchResult.query
|
query = searchResult.query
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import android.text.style.UnderlineSpan
|
|||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
|
val bookUrl: String = "",
|
||||||
val resultCount: Int = 0,
|
val resultCount: Int = 0,
|
||||||
val resultCountWithinChapter: Int = 0,
|
val resultCountWithinChapter: Int = 0,
|
||||||
val resultText: String = "",
|
val resultText: String = "",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
@@ -67,6 +68,12 @@ import kotlin.coroutines.suspendCoroutine
|
|||||||
open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||||
|
|
||||||
companion object {
|
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
|
@Volatile
|
||||||
var hasActiveReadBookRoute: Boolean = false
|
var hasActiveReadBookRoute: Boolean = false
|
||||||
|
|
||||||
@@ -143,10 +150,15 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
private val routeEvents = MutableSharedFlow<NavKey>(extraBufferCapacity = 1)
|
private val routeEvents = MutableSharedFlow<NavKey>(extraBufferCapacity = 1)
|
||||||
private var bookInfoVariableSetter: ((String, String?) -> Unit)? = null
|
private var bookInfoVariableSetter: ((String, String?) -> Unit)? = null
|
||||||
private var shouldApplyDefaultToRead = true
|
private var shouldApplyDefaultToRead = true
|
||||||
|
private var restoredReadBookRoute: MainRouteReadBook? = null
|
||||||
|
private var latestBackStack: List<NavKey> = emptyList()
|
||||||
internal var activeReadBookInputHandler: ReadBookInputHandler? = null
|
internal var activeReadBookInputHandler: ReadBookInputHandler? = null
|
||||||
|
internal var activeReadBookRoute: MainRouteReadBook? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
shouldApplyDefaultToRead = savedInstanceState == null
|
shouldApplyDefaultToRead = savedInstanceState == null
|
||||||
|
restoredReadBookRoute = savedInstanceState?.restoreReadBookRoute()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
if (checkStartupRoute()) return
|
if (checkStartupRoute()) return
|
||||||
@@ -175,6 +187,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
|
if (!intent.hasExplicitStartRoute()) return
|
||||||
routeEvents.tryEmit(MainNavigator.resolveStartRoute(intent))
|
routeEvents.tryEmit(MainNavigator.resolveStartRoute(intent))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,12 +207,21 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val startRoutes = remember {
|
val startRoutes = remember {
|
||||||
resolveInitialStartRoutes(
|
val resolved = MainNavigator.resolveStartRoute(intent)
|
||||||
resolved = MainNavigator.resolveStartRoute(intent),
|
val hasExplicitStartRoute = intent?.hasExplicitStartRoute() == true
|
||||||
defaultToRead = OtherConfig.defaultToRead,
|
when {
|
||||||
isFreshCreate = shouldApplyDefaultToRead,
|
!hasExplicitStartRoute && restoredReadBookRoute != null -> {
|
||||||
).toTypedArray()
|
arrayOf(MainRouteHome, restoredReadBookRoute!!)
|
||||||
|
}
|
||||||
|
shouldApplyDefaultToRead && OtherConfig.defaultToRead && resolved == MainRouteHome -> {
|
||||||
|
arrayOf(MainRouteHome, MainRouteReadBook())
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
arrayOf(resolved)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
latestBackStack = startRoutes.toList()
|
||||||
val backStack = rememberNavBackStack(*startRoutes)
|
val backStack = rememberNavBackStack(*startRoutes)
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
@@ -212,6 +234,11 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(backStack) {
|
||||||
|
snapshotFlow { backStack.toList() }
|
||||||
|
.collect { latestBackStack = it }
|
||||||
|
}
|
||||||
|
|
||||||
SharedTransitionLayout {
|
SharedTransitionLayout {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
@@ -374,6 +401,29 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
if (AppConfig.autoRefreshBook) {
|
if (AppConfig.autoRefreshBook) {
|
||||||
outState.putBoolean("isAutoRefreshedBook", true)
|
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 {
|
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<NavKey> {
|
|
||||||
return if (isFreshCreate && defaultToRead && resolved == MainRouteHome) {
|
|
||||||
listOf(MainRouteHome, MainRouteReadBook())
|
|
||||||
} else {
|
|
||||||
listOf(resolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LauncherW : MainActivity()
|
class LauncherW : MainActivity()
|
||||||
class Launcher1 : MainActivity()
|
class Launcher1 : MainActivity()
|
||||||
class Launcher2 : MainActivity()
|
class Launcher2 : MainActivity()
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ fun MainActivity.mainEntryProvider(
|
|||||||
|
|
||||||
DisposableEffect(controller, lifecycleOwner, route.readAloud) {
|
DisposableEffect(controller, lifecycleOwner, route.readAloud) {
|
||||||
activeReadBookInputHandler = controller
|
activeReadBookInputHandler = controller
|
||||||
|
activeReadBookRoute = route
|
||||||
MainActivity.hasActiveReadBookRoute = true
|
MainActivity.hasActiveReadBookRoute = true
|
||||||
controller.onClose = { onNavigateBack() }
|
controller.onClose = { onNavigateBack() }
|
||||||
controller.onStartContentLoadFinish = {
|
controller.onStartContentLoadFinish = {
|
||||||
@@ -335,6 +336,9 @@ fun MainActivity.mainEntryProvider(
|
|||||||
if (activeReadBookInputHandler === controller) {
|
if (activeReadBookInputHandler === controller) {
|
||||||
activeReadBookInputHandler = null
|
activeReadBookInputHandler = null
|
||||||
}
|
}
|
||||||
|
if (activeReadBookRoute == route) {
|
||||||
|
activeReadBookRoute = null
|
||||||
|
}
|
||||||
MainActivity.hasActiveReadBookRoute = false
|
MainActivity.hasActiveReadBookRoute = false
|
||||||
controller.clearTts()
|
controller.clearTts()
|
||||||
}
|
}
|
||||||
@@ -354,6 +358,7 @@ fun MainActivity.mainEntryProvider(
|
|||||||
|
|
||||||
entry<MainRouteSearchContent> { route ->
|
entry<MainRouteSearchContent> { route ->
|
||||||
val viewModel = koinViewModel<SearchContentViewModel>(
|
val viewModel = koinViewModel<SearchContentViewModel>(
|
||||||
|
key = route.bookUrl,
|
||||||
parameters = { parametersOf(route) }
|
parameters = { parametersOf(route) }
|
||||||
)
|
)
|
||||||
SearchContentScreen(
|
SearchContentScreen(
|
||||||
|
|||||||
@@ -352,6 +352,7 @@
|
|||||||
<string name="padding_bottom">下边距</string>
|
<string name="padding_bottom">下边距</string>
|
||||||
<string name="padding_left">左边距</string>
|
<string name="padding_left">左边距</string>
|
||||||
<string name="padding_right">右边距</string>
|
<string name="padding_right">右边距</string>
|
||||||
|
<string name="padding_format">上: %1$d 下: %2$d 左: %3$d 右: %4$d</string>
|
||||||
<string name="check_book_source">校验书源</string>
|
<string name="check_book_source">校验书源</string>
|
||||||
<string name="check_select_source">校验所选</string>
|
<string name="check_select_source">校验所选</string>
|
||||||
<string name="progress_show">%1$s 进度 %2$d/%3$d</string>
|
<string name="progress_show">%1$s 进度 %2$d/%3$d</string>
|
||||||
|
|||||||
@@ -299,6 +299,7 @@
|
|||||||
<string name="padding_bottom">下邊距</string>
|
<string name="padding_bottom">下邊距</string>
|
||||||
<string name="padding_left">左邊距</string>
|
<string name="padding_left">左邊距</string>
|
||||||
<string name="padding_right">右邊距</string>
|
<string name="padding_right">右邊距</string>
|
||||||
|
<string name="padding_format">上: %1$d 下: %2$d 左: %3$d 右: %4$d</string>
|
||||||
<string name="check_book_source">校驗書源</string>
|
<string name="check_book_source">校驗書源</string>
|
||||||
<string name="check_select_source">校驗所選</string>
|
<string name="check_select_source">校驗所選</string>
|
||||||
<string name="progress_show">%1$s 進度 %2$d/%3$d</string>
|
<string name="progress_show">%1$s 進度 %2$d/%3$d</string>
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
<string name="padding_bottom">下邊距</string>
|
<string name="padding_bottom">下邊距</string>
|
||||||
<string name="padding_left">左邊距</string>
|
<string name="padding_left">左邊距</string>
|
||||||
<string name="padding_right">右邊距</string>
|
<string name="padding_right">右邊距</string>
|
||||||
|
<string name="padding_format">上: %1$d 下: %2$d 左: %3$d 右: %4$d</string>
|
||||||
<string name="check_book_source">校驗書源</string>
|
<string name="check_book_source">校驗書源</string>
|
||||||
<string name="check_select_source">校驗所選</string>
|
<string name="check_select_source">校驗所選</string>
|
||||||
<string name="progress_show">%1$s 進度 %2$d/%3$d</string>
|
<string name="progress_show">%1$s 進度 %2$d/%3$d</string>
|
||||||
|
|||||||
@@ -373,6 +373,7 @@
|
|||||||
<string name="padding_bottom">Bottom</string>
|
<string name="padding_bottom">Bottom</string>
|
||||||
<string name="padding_left">Left</string>
|
<string name="padding_left">Left</string>
|
||||||
<string name="padding_right">Right</string>
|
<string name="padding_right">Right</string>
|
||||||
|
<string name="padding_format">Top: %1$d Bottom: %2$d Left: %3$d Right: %4$d</string>
|
||||||
<string name="check_book_source">Check book sources</string>
|
<string name="check_book_source">Check book sources</string>
|
||||||
<string name="check_select_source">Check the selected source</string>
|
<string name="check_select_source">Check the selected source</string>
|
||||||
<string name="progress_show">%1$s Progress %2$d/%3$d</string>
|
<string name="progress_show">%1$s Progress %2$d/%3$d</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user