Merge branch 'gedoor:master' into master

This commit is contained in:
Xwite
2022-09-04 13:11:54 +08:00
committed by GitHub
115 changed files with 1055 additions and 1521 deletions
@@ -180,7 +180,7 @@ object BookController {
?: return returnData.setErrorMsg("未找到书源")
try {
content = runBlocking {
WebBook.getContentAwait(this, bookSource, book, chapter).let {
WebBook.getContentAwait(bookSource, book, chapter).let {
val contentProcessor = ContentProcessor.get(book.name, book.origin)
contentProcessor.getContent(book, chapter, it, includeTitle = false)
.joinToString("\n")
@@ -5,6 +5,7 @@ import android.text.TextUtils
import io.legado.app.api.ReturnData
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.SourceConfig
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray
@@ -70,6 +71,7 @@ object BookSourceController {
GSON.fromJsonArray<BookSource>(postData).getOrThrow()?.let {
it.forEach { source ->
appDb.bookSourceDao.delete(source)
SourceConfig.removeSource(source.bookSourceUrl)
}
}
}.onFailure {
@@ -109,6 +109,7 @@ object PreferKey {
const val welcomeShowIcon = "welcomeShowIcon"
const val welcomeShowIconDark = "welcomeShowIconDark"
const val pageTouchSlop = "pageTouchSlop"
const val showAddToShelfAlert = "showAddToShelfAlert"
const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent"
@@ -44,6 +44,15 @@ interface RssSourceDao {
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
fun flowEnabled(): Flow<List<RssSource>>
@Query("SELECT * FROM rssSources where enabled = 0 order by customOrder")
fun flowDisabled(): Flow<List<RssSource>>
@Query("select * from rssSources where loginUrl is not null and loginUrl != ''")
fun flowLogin(): Flow<List<RssSource>>
@Query("select * from rssSources where sourceGroup is null or sourceGroup = '' or sourceGroup like '%未分组%'")
fun flowNoGroup(): Flow<List<RssSource>>
@Query(
"""SELECT * FROM rssSources
where enabled = 1
@@ -67,6 +76,9 @@ interface RssSourceDao {
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
fun flowGroup(): Flow<List<String>>
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> '' and enabled = 1")
fun flowGroupEnabled(): Flow<List<String>>
@get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
val allGroup: List<String>
@@ -165,6 +165,7 @@ object SourceAnalyzer {
}
source.loginCheckJs = sourceAny.loginCheckJs
source.bookSourceComment = sourceAny.bookSourceComment
source.variableComment = sourceAny.variableComment
source.lastUpdateTime = sourceAny.lastUpdateTime
source.respondTime = sourceAny.respondTime
source.weight = sourceAny.weight
@@ -220,13 +221,14 @@ object SourceAnalyzer {
var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现
var enabledCookieJar: Boolean = false, // 启用CookieJar
var enabledCookieJar: Boolean = false, // 启用CookieJar
var concurrentRate: String? = null, // 并发率
var header: String? = null, // 请求头
var loginUrl: Any? = null, // 登录规则
var loginUi: Any? = null, // 登录UI
var loginCheckJs: String? = null, //登录检测js
var bookSourceComment: String? = "", //书源注释
var loginCheckJs: String? = null, // 登录检测js
var bookSourceComment: String? = "", // 书源注释
var variableComment: String? = null, // 变量说明
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
var respondTime: Long = 180000L, // 响应时间,用于排序
var weight: Int = 0, // 智能排序的权重
@@ -27,6 +27,7 @@ object SourceVerificationHelp {
appCtx.startActivity<VerificationCodeActivity> {
putExtra("imageUrl", url)
putExtra("sourceOrigin", source.getKey())
putExtra("sourceName", source.getTag())
}
} else {
startBrowser(source, url, title, true)
@@ -58,6 +59,7 @@ object SourceVerificationHelp {
putExtra("title", title)
putExtra("url", url)
putExtra("sourceOrigin", source.getKey())
putExtra("sourceName", source.getTag())
putExtra("sourceVerificationEnable", saveResult)
IntentData.put(url, source.getHeaderMap(true))
}
@@ -310,7 +310,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog)
val loadCoverOnlyWifi = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)
val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)
val showAddToShelfAlert get() = appCtx.getPrefBoolean(PreferKey.showAddToShelfAlert, true)
val doublePageHorizontal: String?
get() = appCtx.getPrefString(PreferKey.doublePageHorizontal)
@@ -0,0 +1,45 @@
package io.legado.app.help.config
import android.content.Context.MODE_PRIVATE
import androidx.core.content.edit
import splitties.init.appCtx
object SourceConfig {
private val sp = appCtx.getSharedPreferences("SourceConfig", MODE_PRIVATE)
fun setBookScore(origin: String, name: String, author: String, score: Int) {
sp.edit {
val preScore = getBookScore(origin, name, author)
var newScore = score
if (preScore != 0) {
newScore = score - preScore
}
putInt(origin, getSourceScore(origin) + newScore)
putInt("${origin}_${name}_${author}", score)
}
}
fun getBookScore(origin: String, name: String, author: String): Int {
return sp.getInt("${origin}_${name}_${author}", 0)
}
fun getSourceScore(origin: String): Int {
return sp.getInt(origin, 0)
}
fun removeSource(origin: String) {
sp.all.keys.filter {
it.startsWith(origin)
}.let {
sp.edit {
it.forEach {
remove(it)
}
}
}
}
}
@@ -105,7 +105,7 @@ object AudioPlay {
book.durChapterIndex = index
book.durChapterPos = 0
durChapter = null
saveRead(book)
saveRead()
play(context)
}
}
@@ -120,7 +120,7 @@ object AudioPlay {
book.durChapterIndex = book.durChapterIndex - 1
book.durChapterPos = 0
durChapter = null
saveRead(book)
saveRead()
play(context)
}
}
@@ -134,17 +134,11 @@ object AudioPlay {
book.durChapterIndex = book.durChapterIndex + 1
book.durChapterPos = 0
durChapter = null
saveRead(book)
saveRead()
play(context)
}
}
fun addTimer() {
val intent = Intent(appCtx, AudioPlayService::class.java)
intent.action = IntentAction.addTimer
appCtx.startService(intent)
}
fun setTimer(minute: Int) {
val intent = Intent(appCtx, AudioPlayService::class.java)
intent.action = IntentAction.setTimer
@@ -152,16 +146,18 @@ object AudioPlay {
appCtx.startService(intent)
}
fun saveRead(book: Book) {
book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis()
Coroutine.async {
appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
book.durChapterTitle = it.getDisplayTitle(
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
)
fun saveRead() {
book?.let { book ->
book.lastCheckCount = 0
book.durChapterTime = System.currentTimeMillis()
Coroutine.async {
appDb.bookChapterDao.getChapter(book.bookUrl, book.durChapterIndex)?.let {
book.durChapterTitle = it.getDisplayTitle(
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
)
}
book.save()
}
book.save()
}
}
@@ -39,13 +39,14 @@ object ReadAloud {
fun play(
context: Context,
play: Boolean = true,
pageIndex: Int = ReadBook.durPageIndex
pageIndex: Int = ReadBook.durPageIndex,
startPos: Int = 0
) {
val intent = Intent(context, aloudClass)
intent.action = IntentAction.play
intent.putExtra("play", play)
intent.putExtra("pageIndex", pageIndex)
intent.putExtra("startPos", startPos)
context.startService(intent)
}
@@ -19,6 +19,7 @@ import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.net.URLDecoder
import java.nio.charset.Charset
class EpubFile(var book: Book) {
@@ -116,7 +117,9 @@ class EpubFile(var book: Book) {
* <image width="1038" height="670" xlink:href="..."/>
* ...titlepage.xhtml
*/
if (chapter.url.contains("titlepage.xhtml")) {
if (chapter.url.contains("titlepage.xhtml") ||
chapter.url.contains("cover.xhtml")
) {
return "<img src=\"cover.jpeg\" />"
}
/*获取当前章节文本*/
@@ -189,7 +192,7 @@ class EpubFile(var book: Book) {
private fun getImage(href: String): InputStream? {
if (href == "cover.jpeg") return epubBook?.coverImage?.inputStream
val abHref = href.replace("../", "")
val abHref = URLDecoder.decode(href.replace("../", ""), "UTF-8")
return epubBook?.resources?.getByHref(abHref)?.inputStream
}
@@ -6,6 +6,7 @@ import androidx.documentfile.provider.DocumentFile
import com.script.SimpleBindings
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.BaseSource
import io.legado.app.data.entities.Book
@@ -16,6 +17,7 @@ import io.legado.app.help.BookHelp
import io.legado.app.help.config.AppConfig
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.*
import org.jsoup.nodes.Entities
import splitties.init.appCtx
import java.io.*
import java.util.regex.Pattern
@@ -77,11 +79,12 @@ object LocalBook {
throw TocEmptyException(appCtx.getString(R.string.chapter_list_empty))
}
val lh = LinkedHashSet(chapters)
lh.forEachIndexed { index, bookChapter -> bookChapter.index = index }
return ArrayList(lh)
}
fun getContent(book: Book, chapter: BookChapter): String? {
return try {
val content = try {
when {
book.isEpub() -> {
EpubFile.getContent(book, chapter)
@@ -95,8 +98,15 @@ object LocalBook {
}
} catch (e: Exception) {
e.printOnDebug()
e.localizedMessage
}
AppLog.put("获取本地书籍内容失败\n${e.localizedMessage}", e)
"获取本地书籍内容失败\n${e.localizedMessage}"
}?.replace("&lt;img", "&lt; img", true)
content ?: return null
return kotlin.runCatching {
Entities.unescape(content)
}.onFailure {
AppLog.put("HTML实体解码失败\n${it.localizedMessage}", it)
}.getOrElse { content }
}
/**
@@ -111,6 +111,7 @@ object BookContent {
}
}
var contentStr = content.toString()
//全文替换
val replaceRegex = contentRule.replaceRegex
if (!replaceRegex.isNullOrEmpty()) {
contentStr = analyzeRule.getString(replaceRegex, contentStr)
@@ -238,12 +238,11 @@ object WebBook {
context: CoroutineContext = Dispatchers.IO
): Coroutine<String> {
return Coroutine.async(scope, context) {
getContentAwait(scope, bookSource, book, bookChapter, nextChapterUrl, needSave)
getContentAwait(bookSource, book, bookChapter, nextChapterUrl, needSave)
}
}
suspend fun getContentAwait(
scope: CoroutineScope,
bookSource: BookSource,
book: Book,
bookChapter: BookChapter,
@@ -118,24 +118,19 @@ abstract class BaseReadAloudService : BaseService(),
}
private fun newReadAloud(play: Boolean, pageIndex: Int, startPos: Int) {
this.pageIndex = pageIndex
textChapter = ReadBook.curTextChapter
textChapter?.let { textChapter ->
nowSpeak = 0
readAloudNumber = textChapter.getReadLength(pageIndex) + startPos
contentList.clear()
if (getPrefBoolean(PreferKey.readAloudByPage)) {
for (index in pageIndex..textChapter.lastIndex) {
textChapter.getPage(index)?.text?.split("\n")?.let {
contentList.addAll(it)
val readAloudByPage = getPrefBoolean(PreferKey.readAloudByPage)
textChapter.getNeedReadAloud(pageIndex, readAloudByPage, startPos)
.split("\n").forEach { text ->
if (text.isNotEmpty()) {
contentList.add(text)
}
}
} else {
textChapter.getUnRead(pageIndex).split("\n").forEach {
if (it.isNotEmpty()) {
contentList.add(it)
}
}
}
if (play) play()
}
}
@@ -136,7 +136,7 @@ class CheckSourceService : BaseService() {
source.bookSourceComment = source.bookSourceComment
?.split("\n\n")
?.filterNot {
it.startsWith("Error: ")
it.startsWith("// Error: ")
}?.joinToString("\n")
//校验搜索书籍
if (CheckSource.checkSearch) {
@@ -186,7 +186,7 @@ class CheckSourceService : BaseService() {
!is NoStackTraceException -> source.addGroup("网站失效")
}
source.bookSourceComment =
"Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank())
"// Error: ${it.localizedMessage}" + if (source.bookSourceComment.isNullOrBlank())
"" else "\n\n${source.bookSourceComment}"
Debug.updateFinalMessage(source.bookSourceUrl, "校验失败:${it.localizedMessage}")
}.onSuccess(searchCoroutine) {
@@ -219,7 +219,6 @@ class CheckSourceService : BaseService() {
//校验正文
if (CheckSource.checkContent) {
WebBook.getContentAwait(
this,
bookSource = source,
book = mBook,
bookChapter = toc.first(),
@@ -1,7 +1,7 @@
package io.legado.app.ui.about
import java.text.SimpleDateFormat
import android.content.Context
import android.icu.text.SimpleDateFormat
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@@ -46,7 +46,7 @@ class OnLineImportActivity :
finallyDialog(getString(R.string.error), it)
}
intent.data?.let {
val url = it.getQueryParameter("src")
val url = it.query?.substringAfter("src=")
if (url.isNullOrBlank()) {
finish()
return
@@ -2,12 +2,12 @@ package io.legado.app.ui.association
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.MenuItem
import androidx.appcompat.widget.Toolbar
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import io.legado.app.R
import io.legado.app.base.BaseDialogFragment
import io.legado.app.databinding.DialogVerificationCodeViewBinding
@@ -17,7 +17,9 @@ import io.legado.app.help.glide.ImageLoader
import io.legado.app.help.glide.OkHttpModelLoader
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.*
import io.legado.app.utils.applyTint
import io.legado.app.utils.setLayout
import io.legado.app.utils.showDialogFragment
import io.legado.app.utils.viewbindingdelegate.viewBinding
/**
@@ -26,12 +28,18 @@ import io.legado.app.utils.viewbindingdelegate.viewBinding
* val key = "${sourceOrigin ?: ""}_verificationResult"
* CacheManager.get(key)
*/
class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view), Toolbar.OnMenuItemClickListener {
class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification_code_view),
Toolbar.OnMenuItemClickListener {
constructor(imageUrl: String, sourceOrigin: String? = null) : this() {
constructor(
imageUrl: String,
sourceOrigin: String? = null,
sourceName: String? = null
) : this() {
arguments = Bundle().apply {
putString("sourceOrigin", sourceOrigin)
putString("imageUrl", imageUrl)
putString("sourceOrigin", sourceOrigin)
putString("sourceName", sourceName)
}
}
@@ -47,23 +55,23 @@ class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification
initMenu()
binding.run {
toolBar.setBackgroundColor(primaryColor)
val sourceOrigin = arguments?.getString("sourceOrigin")
arguments?.getString("imageUrl")?.let { imageUrl ->
ImageLoader.load(requireContext(), imageUrl).apply {
sourceOrigin?.let {
apply(
RequestOptions().set(
OkHttpModelLoader.sourceOriginOption,
it
arguments?.let { arguments ->
toolBar.subtitle = arguments.getString("sourceName")
val sourceOrigin = arguments.getString("sourceOrigin")
arguments.getString("imageUrl")?.let { imageUrl ->
ImageLoader.load(requireContext(), imageUrl).apply {
sourceOrigin?.let {
apply(
RequestOptions().set(OkHttpModelLoader.sourceOriginOption, it)
)
)
}
}.error(R.drawable.image_loading_error)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(verificationCodeImageView)
verificationCodeImageView.setOnClickListener {
showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
}
}.error(R.drawable.image_loading_error)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(verificationCodeImageView)
verificationCodeImageView.setOnClickListener {
showDialogFragment(PhotoDialog(imageUrl, sourceOrigin))
}
}
}
@@ -86,7 +94,7 @@ class VerificationCodeDialog() : BaseDialogFragment(R.layout.dialog_verification
CacheManager.putMemory(key, it)
dismiss()
}
}
}
}
return false
}
@@ -37,9 +37,9 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
loadChapterList(book)
}
}
saveRead(book)
}
}
saveRead()
}
}
@@ -5,14 +5,19 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.R
import io.legado.app.base.adapter.DiffRecyclerAdapter
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemChangeSourceBinding
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import splitties.init.appCtx
import splitties.views.onLongClick
@@ -68,10 +73,57 @@ class ChangeBookSourceAdapter(
}
}
}
val score = callBack.getBookScore(item)
if (score > 0) {
binding.ivBad.gone()
binding.ivGood.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
} else if (score < 0) {
binding.ivGood.gone()
binding.ivBad.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
} else {
binding.ivGood.visible()
binding.ivBad.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
binding.ivGood.setOnClickListener {
if (binding.ivBad.isVisible) {
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
binding.ivBad.gone()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 1)
}
} else {
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
binding.ivBad.visible()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 0)
}
}
}
binding.ivBad.setOnClickListener {
if (binding.ivGood.isVisible) {
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
binding.ivGood.gone()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, -1)
}
} else {
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
binding.ivGood.visible()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 0)
}
}
}
holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let {
if (it.bookUrl != callBack.bookUrl) {
@@ -120,5 +172,7 @@ class ChangeBookSourceAdapter(
fun editSource(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook)
fun deleteSource(searchBook: SearchBook)
fun setBookScore(searchBook: SearchBook, score: Int)
fun getBookScore(searchBook: SearchBook): Int
}
}
@@ -294,6 +294,14 @@ class ChangeBookSourceDialog() : BaseDialogFragment(R.layout.dialog_book_change_
}
}
override fun setBookScore(searchBook: SearchBook, score: Int) {
viewModel.setBookScore(searchBook,score)
}
override fun getBookScore(searchBook: SearchBook): Int {
return viewModel.getBookScore(searchBook)
}
private fun changeSource(searchBook: SearchBook, onSuccess: (() -> Unit)? = null) {
waitDialog.setText(R.string.load_toc)
waitDialog.show()
@@ -16,6 +16,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.SourceConfig
import io.legado.app.help.coroutine.CompositeCoroutine
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.webBook.WebBook
@@ -82,7 +83,23 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
searchCallback = null
}
}.map {
searchBooks.sortedBy { it.originOrder }
searchBooks.sortedWith { o1, o2 ->
val o1bs = SourceConfig.getBookScore(o1.origin, o1.name, o1.author)
val o2bs = SourceConfig.getBookScore(o2.origin, o2.name, o2.author)
when {
o1bs - o2bs > 0 -> -1
o1bs - o2bs < 0 -> 1
else -> {
val o1ss = SourceConfig.getSourceScore(o1.origin)
val o2ss = SourceConfig.getSourceScore(o2.origin)
when {
o1ss - o2ss > 0 -> -1
o1ss - o2ss < 0 -> 1
else -> o1.originOrder - o2.originOrder
}
}
}
}
}.flowOn(IO)
@Volatile
@@ -351,6 +368,7 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
appDb.bookSourceDao.delete(source)
appDb.searchBookDao.delete(searchBook)
SourceConfig.removeSource(source.bookSourceUrl)
}
}
searchBooks.remove(searchBook)
@@ -379,6 +397,17 @@ open class ChangeBookSourceViewModel(application: Application) : BaseViewModel(a
}
}
fun setBookScore(searchBook: SearchBook, score: Int) {
execute {
SourceConfig.setBookScore(searchBook.origin, searchBook.name, searchBook.author, score)
searchCallback?.upAdapter()
}
}
fun getBookScore(searchBook: SearchBook): Int {
return SourceConfig.getBookScore(searchBook.origin, searchBook.name, searchBook.author)
}
interface SourceCallback {
fun searchSuccess(searchBook: SearchBook)
@@ -5,14 +5,19 @@ import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import io.legado.app.R
import io.legado.app.base.adapter.DiffRecyclerAdapter
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemChangeSourceBinding
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.gone
import io.legado.app.utils.invisible
import io.legado.app.utils.visible
import splitties.init.appCtx
import splitties.views.onLongClick
@@ -68,10 +73,57 @@ class ChangeChapterSourceAdapter(
}
}
}
val score = callBack.getBookScore(item)
if (score > 0) {
binding.ivBad.gone()
binding.ivGood.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
} else if (score < 0) {
binding.ivGood.gone()
binding.ivBad.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
} else {
binding.ivGood.visible()
binding.ivBad.visible()
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
binding.ivGood.setOnClickListener {
if (binding.ivBad.isVisible) {
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_A200))
binding.ivBad.gone()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 1)
}
} else {
DrawableCompat.setTint(binding.ivGood.drawable, appCtx.getCompatColor(R.color.md_red_100))
binding.ivBad.visible()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 0)
}
}
}
binding.ivBad.setOnClickListener {
if (binding.ivGood.isVisible) {
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_A200))
binding.ivGood.gone()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, -1)
}
} else {
DrawableCompat.setTint(binding.ivBad.drawable, appCtx.getCompatColor(R.color.md_blue_100))
binding.ivGood.visible()
getItem(holder.layoutPosition)?.let {
callBack.setBookScore(it, 0)
}
}
}
holder.itemView.setOnClickListener {
getItem(holder.layoutPosition)?.let {
callBack.openToc(it)
@@ -118,5 +170,7 @@ class ChangeChapterSourceAdapter(
fun editSource(searchBook: SearchBook)
fun disableSource(searchBook: SearchBook)
fun deleteSource(searchBook: SearchBook)
fun setBookScore(searchBook: SearchBook, score: Int)
fun getBookScore(searchBook: SearchBook): Int
}
}
@@ -314,6 +314,14 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
}
}
override fun setBookScore(searchBook: SearchBook, score: Int) {
viewModel.setBookScore(searchBook, score)
}
override fun getBookScore(searchBook: SearchBook): Int {
return viewModel.getBookScore(searchBook)
}
override fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) {
searchBook?.let {
binding.loadingToc.show()
@@ -35,7 +35,7 @@ class ChangeChapterSourceViewModel(application: Application) :
execute {
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
?: throw NoStackTraceException("书源不存在")
WebBook.getContentAwait(this, bookSource, book, chapter, nextChapterUrl, false)
WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false)
}.onSuccess {
success.invoke(it)
}.onError {
@@ -21,6 +21,7 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
import io.legado.app.databinding.ActivityBookInfoBinding
import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.bottomBackground
@@ -247,8 +248,10 @@ class BookInfoActivity :
private fun showCover(book: Book) {
binding.ivCover.load(book.getDisplayCover(), book.name, book.author, false, book.origin)
BookCover.loadBlur(this, book.getDisplayCover())
.into(binding.bgBook)
if(!AppConfig.isEInkMode) {
BookCover.loadBlur(this, book.getDisplayCover())
.into(binding.bgBook)
}
}
private fun upLoading(isLoading: Boolean, chapterList: List<BookChapter>? = null) {
@@ -14,6 +14,8 @@ import io.legado.app.databinding.ItemArrangeBookBinding
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
import io.legado.app.ui.widget.recycler.ItemTouchCallback
import java.util.*
import kotlin.collections.HashSet
class BookAdapter(context: Context, val callBack: CallBack) :
RecyclerAdapter<Book, ItemArrangeBookBinding>(context),
@@ -121,6 +123,25 @@ class BookAdapter(context: Context, val callBack: CallBack) :
callBack.upSelectCount()
}
fun checkSelectedInterval() {
val selectedPosition = linkedSetOf<Int>()
getItems().forEachIndexed { index, it ->
if (selectedBooks.contains(it)) {
selectedPosition.add(index)
}
}
val minPosition = Collections.min(selectedPosition)
val maxPosition = Collections.max(selectedPosition)
val itemCount = maxPosition - minPosition + 1
for (i in minPosition..maxPosition) {
getItem(i)?.let {
selectedBooks.add(it)
}
}
notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
callBack.upSelectCount()
}
private fun getGroupList(groupId: Long): List<String> {
val groupNames = arrayListOf<String>()
callBack.groupList.forEach {
@@ -192,6 +192,7 @@ class BookshelfManageActivity :
viewModel.upCanUpdate(adapter.selection, false)
R.id.menu_add_to_group -> selectGroup(addToGroupRequestCode, 0)
R.id.menu_change_source -> showDialogFragment<SourcePickerDialog>()
R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
}
return false
}
@@ -126,7 +126,7 @@ class ContentEditDialog : BaseDialogFragment(R.layout.dialog_content_edit) {
content = null
BookHelp.delContent(book, chapter)
if (!book.isLocalBook()) ReadBook.bookSource?.let { bookSource ->
WebBook.getContentAwait(this, bookSource, book, chapter)
WebBook.getContentAwait(bookSource, book, chapter)
}
}
return@execute content ?: let {
@@ -25,6 +25,7 @@ import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.BookHelp
import io.legado.app.help.IntentData
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadTipConfig
import io.legado.app.help.coroutine.Coroutine
@@ -35,6 +36,7 @@ import io.legado.app.lib.dialogs.selector
import io.legado.app.lib.theme.accentColor
import io.legado.app.model.ReadAloud
import io.legado.app.model.ReadBook
import io.legado.app.model.analyzeRule.AnalyzeRule
import io.legado.app.receiver.TimeBatteryReceiver
import io.legado.app.service.BaseReadAloudService
import io.legado.app.ui.about.AppLogDialog
@@ -182,6 +184,7 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
upSystemUiVisibility()
binding.readMenu.upBrightnessState()
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -960,31 +963,41 @@ class ReadBookActivity : BaseReadBookActivity(),
}
override fun payAction() {
Coroutine.async(this) {
val book = ReadBook.book ?: throw NoStackTraceException("no book")
ReadBook.book?.let { book ->
if (book.isLocalBook()) return
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
?: throw NoStackTraceException("no chapter")
val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
val payAction = source.getContentRule().payAction
if (payAction.isNullOrEmpty()) {
throw NoStackTraceException("no pay action")
if (chapter == null) {
toastOnUi("no chapter")
return
}
JsUtils.evalJs(payAction) {
it["java"] = source
it["source"] = source
it["book"] = book
it["chapter"] = chapter
}
}.onSuccess {
if (it.isNotBlank()) {
startActivity<WebViewActivity> {
putExtra("title", getString(R.string.chapter_pay))
putExtra("url", it)
IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
alert(R.string.chapter_pay) {
setMessage(chapter.title)
yesButton {
Coroutine.async {
val source = ReadBook.bookSource ?: throw NoStackTraceException("no book source")
val payAction = source.getContentRule().payAction
if (payAction.isNullOrEmpty()) {
throw NoStackTraceException("no pay action")
}
val analyzeRule = AnalyzeRule(book, source)
analyzeRule.setBaseUrl(chapter.url)
analyzeRule.chapter = chapter
analyzeRule.evalJS(payAction).toString()
}.onSuccess {
if (it.isNotBlank()) {
startActivity<WebViewActivity> {
putExtra("title", R.string.chapter_pay)
putExtra("url", it)
IntentData.put(it, ReadBook.bookSource?.getHeaderMap(true))
}
}
}.onError {
AppLog.putDebug(it.localizedMessage)
toastOnUi(it.localizedMessage)
}
}
noButton()
}
}.onError {
toastOnUi(it.localizedMessage)
}
}
@@ -1147,13 +1160,17 @@ class ReadBookActivity : BaseReadBookActivity(),
override fun finish() {
ReadBook.book?.let {
if (!ReadBook.inBookshelf) {
alert(title = getString(R.string.add_to_shelf)) {
setMessage(getString(R.string.check_add_bookshelf, it.name))
okButton {
ReadBook.inBookshelf = true
setResult(Activity.RESULT_OK)
if (!AppConfig.showAddToShelfAlert) {
viewModel.removeFromBookshelf { super.finish() }
} else {
alert(title = getString(R.string.add_to_shelf)) {
setMessage(getString(R.string.check_add_bookshelf, it.name))
okButton {
ReadBook.inBookshelf = true
setResult(Activity.RESULT_OK)
}
noButton { viewModel.removeFromBookshelf { super.finish() } }
}
noButton { viewModel.removeFromBookshelf { super.finish() } }
}
} else {
super.finish()
@@ -1190,6 +1207,7 @@ class ReadBookActivity : BaseReadBookActivity(),
}
observeEvent<Boolean>(EventBus.UP_CONFIG) {
upSystemUiVisibility()
readView.upPageSlopSquare()
readView.upBg()
readView.upStyle()
readView.upBgAlpha()
@@ -216,7 +216,6 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
toc.first()
}
WebBook.getContentAwait(
this,
bookSource = source,
book = book,
bookChapter = toc[book.durChapterIndex],
@@ -43,10 +43,18 @@ class ReadMenu @JvmOverloads constructor(
var cnaShowMenu: Boolean = false
private val callBack: CallBack get() = activity as CallBack
private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true)
private lateinit var menuTopIn: Animation
private lateinit var menuTopOut: Animation
private lateinit var menuBottomIn: Animation
private lateinit var menuBottomOut: Animation
private val menuTopIn: Animation by lazy {
loadAnimation(context, R.anim.anim_readbook_top_in)
}
private val menuTopOut: Animation by lazy {
loadAnimation(context, R.anim.anim_readbook_top_out)
}
private val menuBottomIn: Animation by lazy {
loadAnimation(context, R.anim.anim_readbook_bottom_in)
}
private val menuBottomOut: Animation by lazy {
loadAnimation(context, R.anim.anim_readbook_bottom_out)
}
private val bgColor: Int = context.bottomBackground
private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
private val bottomBackgroundList: ColorStateList = Selector.colorBuild()
@@ -71,6 +79,58 @@ class ReadMenu @JvmOverloads constructor(
}
}
}
private val menuInListener = object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
binding.tvSourceAction.isGone = ReadBook.isLocalBook
binding.tvLogin.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
binding.tvPay.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
|| ReadBook.curTextChapter?.isVip != true
|| ReadBook.curTextChapter?.isPay == true
callBack.upSystemUiVisibility()
binding.llBrightness.visible(showBrightnessView)
}
@SuppressLint("RtlHardcoded")
override fun onAnimationEnd(animation: Animation) {
val navigationBarHeight =
if (ReadBookConfig.hideNavigationBar) {
activity?.navigationBarHeight ?: 0
} else {
0
}
binding.run {
vwMenuBg.setOnClickListener { runMenuOut() }
root.padding = 0
when (activity?.navigationBarGravity) {
Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
Gravity.LEFT -> root.leftPadding = navigationBarHeight
Gravity.RIGHT -> root.rightPadding = navigationBarHeight
}
}
callBack.upSystemUiVisibility()
if (!LocalConfig.readMenuHelpVersionIsLast) {
callBack.showReadMenuHelp()
}
}
override fun onAnimationRepeat(animation: Animation) = Unit
}
private val menuOutListener = object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
binding.vwMenuBg.setOnClickListener(null)
}
override fun onAnimationEnd(animation: Animation) {
this@ReadMenu.invisible()
binding.titleBar.invisible()
binding.bottomMenu.invisible()
cnaShowMenu = false
onMenuOutEnd?.invoke()
callBack.upSystemUiVisibility()
}
override fun onAnimationRepeat(animation: Animation) = Unit
}
init {
initView()
@@ -141,19 +201,29 @@ class ReadMenu @JvmOverloads constructor(
activity?.window?.attributes = params
}
fun runMenuIn() {
fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
this.visible()
binding.titleBar.visible()
binding.bottomMenu.visible()
binding.titleBar.startAnimation(menuTopIn)
binding.bottomMenu.startAnimation(menuBottomIn)
if (anim) {
binding.titleBar.startAnimation(menuTopIn)
binding.bottomMenu.startAnimation(menuBottomIn)
} else {
menuInListener.onAnimationStart(menuBottomIn)
menuInListener.onAnimationEnd(menuBottomIn)
}
}
fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) {
fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
this.onMenuOutEnd = onMenuOutEnd
if (this.isVisible) {
binding.titleBar.startAnimation(menuTopOut)
binding.bottomMenu.startAnimation(menuBottomOut)
if (anim) {
binding.titleBar.startAnimation(menuTopOut)
binding.bottomMenu.startAnimation(menuBottomOut)
} else {
menuOutListener.onAnimationStart(menuBottomOut)
menuOutListener.onAnimationEnd(menuBottomOut)
}
}
}
@@ -288,7 +358,9 @@ class ReadMenu @JvmOverloads constructor(
}
}
llReadAloud.onLongClick {
runMenuOut { callBack.showReadAloudDialog() }
runMenuOut {
callBack.showReadAloudDialog()
}
}
//界面
llFont.setOnClickListener {
@@ -306,66 +378,8 @@ class ReadMenu @JvmOverloads constructor(
}
private fun initAnimation() {
//显示菜单
menuTopIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_in)
menuBottomIn = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
menuTopIn.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
binding.tvSourceAction.isGone = ReadBook.isLocalBook
binding.tvLogin.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
binding.tvPay.isGone = ReadBook.bookSource?.loginUrl.isNullOrEmpty()
|| ReadBook.curTextChapter?.isVip != true
|| ReadBook.curTextChapter?.isPay == true
callBack.upSystemUiVisibility()
binding.llBrightness.visible(showBrightnessView)
}
@SuppressLint("RtlHardcoded")
override fun onAnimationEnd(animation: Animation) {
val navigationBarHeight =
if (ReadBookConfig.hideNavigationBar) {
activity?.navigationBarHeight ?: 0
} else {
0
}
binding.run {
vwMenuBg.setOnClickListener { runMenuOut() }
root.padding = 0
when (activity?.navigationBarGravity) {
Gravity.BOTTOM -> root.bottomPadding = navigationBarHeight
Gravity.LEFT -> root.leftPadding = navigationBarHeight
Gravity.RIGHT -> root.rightPadding = navigationBarHeight
}
}
callBack.upSystemUiVisibility()
if (!LocalConfig.readMenuHelpVersionIsLast) {
callBack.showReadMenuHelp()
}
}
override fun onAnimationRepeat(animation: Animation) = Unit
})
//隐藏菜单
menuTopOut = AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_top_out)
menuBottomOut =
AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
menuTopOut.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {
binding.vwMenuBg.setOnClickListener(null)
}
override fun onAnimationEnd(animation: Animation) {
this@ReadMenu.invisible()
binding.titleBar.invisible()
binding.bottomMenu.invisible()
cnaShowMenu = false
onMenuOutEnd?.invoke()
callBack.upSystemUiVisibility()
}
override fun onAnimationRepeat(animation: Animation) = Unit
})
menuTopIn.setAnimationListener(menuInListener)
menuTopOut.setAnimationListener(menuOutListener)
}
fun upBookView() {
@@ -33,10 +33,8 @@ class SearchMenu @JvmOverloads constructor(
private val callBack: CallBack get() = activity as CallBack
private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true)
private val menuBottomIn: Animation =
AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_in)
private val menuBottomOut: Animation =
AnimationUtilsSupport.loadAnimation(context, R.anim.anim_readbook_bottom_out)
private val menuBottomIn: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_in)
private val menuBottomOut: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_out)
private val bgColor: Int = context.bottomBackground
private val textColor: Int = context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
private val bottomBackgroundList: ColorStateList =
@@ -11,6 +11,7 @@ import androidx.preference.Preference
import io.legado.app.R
import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.prefs.fragment.PreferenceFragment
import io.legado.app.lib.theme.bottomBackground
@@ -18,6 +19,7 @@ import io.legado.app.lib.theme.primaryColor
import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.utils.dpToPx
import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.postEvent
@@ -70,9 +72,12 @@ class MoreConfigDialog : DialogFragment() {
class ReadPreferenceFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
@SuppressLint("RestrictedApi")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_config_read)
upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -137,9 +142,29 @@ class MoreConfigDialog : DialogFragment() {
"clickRegionalConfig" -> {
(activity as? ReadBookActivity)?.showClickRegionalConfig()
}
PreferKey.pageTouchSlop -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.page_touch_slop_dialog_title))
.setMaxValue(9999)
.setMinValue(0)
.setValue(AppConfig.pageTouchSlop)
.show {
AppConfig.pageTouchSlop = it
postEvent(EventBus.UP_CONFIG, false)
}
}
}
return super.onPreferenceTreeClick(preference)
}
@Suppress("SameParameterValue")
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
val preference = findPreference<Preference>(preferenceKey) ?: return
when (preferenceKey) {
PreferKey.pageTouchSlop -> preference.summary =
getString(R.string.page_touch_slop_summary, value)
}
}
}
}
@@ -169,12 +169,14 @@ class TocRegexDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
alertBinding.apply {
tvRuleName.setText(tocRule.name)
tvRuleRegex.setText(tocRule.rule)
tvRuleExample.setText(tocRule.example)
}
customView { alertBinding.root }
okButton {
alertBinding.apply {
tocRule.name = tvRuleName.text.toString()
tocRule.rule = tvRuleRegex.text.toString()
tocRule.example = tvRuleExample.text.toString()
viewModel.saveRule(tocRule)
}
}
@@ -200,6 +202,7 @@ class TocRegexDialog() : BaseDialogFragment(R.layout.dialog_toc_regex),
if (payloads.isEmpty()) {
root.setBackgroundColor(context.backgroundColor)
rbRegexName.text = item.name
titleExample.text = item.example
rbRegexName.isChecked = item.name == selectedName
swtEnabled.isChecked = item.enable
} else {
@@ -446,7 +446,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return book.createBookMark().apply {
chapterIndex = page.chapterIndex
chapterPos = chapter.getReadLength(page.index) +
page.getSelectStartLength(selectStart.lineIndex, selectStart.charIndex)
page.getPosByLineColumn(selectStart.lineIndex, selectStart.charIndex)
chapterName = chapter.title
bookText = getSelectedText()
}
@@ -82,13 +82,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
private val initialTextPos = TextPos(0, 0, 0)
val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
val pageSlopSquare by lazy {
if (AppConfig.pageTouchSlop == 0) {
slopSquare
} else {
AppConfig.pageTouchSlop
}
}
private var pageSlopSquare: Int = slopSquare
private val tlRect = RectF()
private val tcRect = RectF()
private val trRect = RectF()
@@ -112,6 +106,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
upBg()
setWillNotDraw(false)
upPageAnim()
upPageSlopSquare()
}
}
@@ -253,6 +248,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
return true
}
/**
* 更新状态栏
*/
fun upStatusBar() {
curPage.upStatusBar()
prevPage.upStatusBar()
@@ -402,6 +400,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
}
}
/**
* 点击
*/
private fun click(action: Int) {
when (action) {
0 -> callBack.showActionMenu()
@@ -441,6 +442,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
}
}
/**
* 销毁事件
*/
fun onDestroy() {
pageDelegate?.onDestroy()
curPage.cancelSelect()
@@ -511,6 +515,14 @@ class ReadView(context: Context, attrs: AttributeSet) :
callBack.screenOffTimerStart()
}
/**
* 更新滑动距离
*/
fun upPageSlopSquare() {
val pageTouchSlop = AppConfig.pageTouchSlop
this.pageSlopSquare = if (pageTouchSlop == 0) slopSquare else pageTouchSlop
}
/**
* 更新样式
*/
@@ -563,10 +575,17 @@ class ReadView(context: Context, attrs: AttributeSet) :
*/
fun aloudStartSelect() {
val selectStartPos = curPage.selectStartPos
if (selectStartPos.relativePagePos > 0) {
ReadBook.moveToNextPage()
var pagePos = selectStartPos.relativePagePos
val line = selectStartPos.lineIndex
val column = selectStartPos.charIndex
while (pagePos > 0) {
if (!ReadBook.moveToNextPage()) {
ReadBook.moveToNextChapter(false)
}
pagePos--
}
//TODO 未完成
val startPos = curPage.textPage.getPosByLineColumn(line, column)
ReadAloud.play(context, startPos = startPos)
}
/**
@@ -2,6 +2,7 @@ package io.legado.app.ui.book.read.page.entities
import kotlin.math.min
@Suppress("unused")
data class TextChapter(
val position: Int,
val title: String,
@@ -61,6 +62,17 @@ data class TextChapter(
return getReadLength(pageIndex + 1)
}
/**
* 获取内容
*/
fun getContent(): String {
val stringBuilder = StringBuilder()
pages.forEach {
stringBuilder.append(it.text)
}
return stringBuilder.toString()
}
/**
* @return 获取未读文字
*/
@@ -75,14 +87,22 @@ data class TextChapter(
}
/**
* 获取内容
* @return 需要朗读的文本列表
* @param pageIndex 起始页
* @param pageSplit 是否分页
* @param startPos 从当前页什么地方开始朗读
*/
fun getContent(): String {
fun getNeedReadAloud(pageIndex: Int, pageSplit: Boolean, startPos: Int): String {
val stringBuilder = StringBuilder()
pages.forEach {
stringBuilder.append(it.text)
if (pages.isNotEmpty()) {
for (index in pageIndex..pages.lastIndex) {
stringBuilder.append(pages[index].text)
if (pageSplit && !stringBuilder.endsWith("\n")) {
stringBuilder.append("\n")
}
}
}
return stringBuilder.toString()
return stringBuilder.substring(startPos).toString()
}
/**
@@ -34,6 +34,9 @@ data class TextPage(
}
}
/**
* 底部对齐更新行位置
*/
fun upLinesPosition() {
if (!ReadBookConfig.textBottomJustify) return
if (textLines.size <= 1) return
@@ -77,6 +80,9 @@ data class TextPage(
}
}
/**
* 计算文字位置
*/
@Suppress("DEPRECATION")
fun format(): TextPage {
if (textLines.isEmpty()) isMsgPage = true
@@ -105,9 +111,7 @@ data class TextPage(
val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
val x1 = x + cw
textLine.textChars.add(
TextChar(
char, start = x, end = x1
)
TextChar(char, start = x, end = x1)
)
x = x1
}
@@ -118,6 +122,9 @@ data class TextPage(
return this
}
/**
* 移除朗读标志
*/
fun removePageAloudSpan(): TextPage {
textLines.forEach { textLine ->
textLine.isReadAloud = false
@@ -125,6 +132,10 @@ data class TextPage(
return this
}
/**
* 更新朗读标志
* @param aloudSpanStart 朗读文字开始位置
*/
fun upPageAloudSpan(aloudSpanStart: Int) {
removePageAloudSpan()
var lineStart = 0
@@ -151,6 +162,9 @@ data class TextPage(
}
}
/**
* 阅读进度
*/
val readProgress: String
get() {
val df = DecimalFormat("0.0%")
@@ -167,15 +181,24 @@ data class TextPage(
return percent
}
fun getSelectStartLength(lineIndex: Int, charIndex: Int): Int {
/**
* 根据行和列返回字符在本页的位置
* @param lineIndex 字符在第几行
* @param columnIndex 字符在第几列
* @return 字符在本页位置
*/
fun getPosByLineColumn(lineIndex: Int, columnIndex: Int): Int {
var length = 0
val maxIndex = min(lineIndex, lineSize)
for (index in 0 until maxIndex) {
length += textLines[index].charSize
}
return length + charIndex
return length + columnIndex
}
/**
* @return 页面所在章节
*/
fun getTextChapter(): TextChapter? {
ReadBook.curTextChapter?.let {
if (it.position == chapterIndex) {
@@ -66,6 +66,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
private var menu: Menu? = null
private var precisionSearchMenuItem: MenuItem? = null
private var groups = linkedSetOf<String>()
private var isManualStopSearch = false
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = searchFinish@{ isEmpty ->
val searchGroup = AppConfig.searchGroup
if (!isEmpty || searchGroup.isEmpty()) return@searchFinish
@@ -155,6 +156,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
override fun onQueryTextSubmit(query: String?): Boolean {
searchView.clearFocus()
query?.let {
isManualStopSearch = false
viewModel.saveSearchKey(query)
viewModel.searchKey = ""
viewModel.search(it)
@@ -221,6 +223,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
.setPressedColor(ColorUtils.darkenColor(accentColor))
.create()
binding.fbStop.setOnClickListener {
isManualStopSearch = true
viewModel.stop()
binding.refreshProgressBar.isAutoLoading = false
}
@@ -266,6 +269,9 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
* 滚动到底部事件
*/
private fun scrollToBottom() {
if (isManualStopSearch) {
return
}
if (viewModel.isSearchLiveData.value == false
&& viewModel.searchKey.isNotEmpty()
&& loadMoreView.hasMore
@@ -335,7 +341,7 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
historyFlowJob?.cancel()
historyFlowJob = launch {
when {
key.isNullOrBlank() -> appDb.searchKeywordDao.flowByUsage()
key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime()
else -> appDb.searchKeywordDao.flowSearch(key)
}.conflate().collect {
historyKeyAdapter.setItems(it)
@@ -7,6 +7,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.RuleComplete
import io.legado.app.help.config.SourceConfig
import io.legado.app.help.http.CookieStore
import io.legado.app.help.http.newCallStrResponse
import io.legado.app.help.http.okHttpClient
@@ -40,6 +41,7 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
oldSourceUrl?.let {
if (oldSourceUrl != source.bookSourceUrl) {
appDb.bookSourceDao.delete(it)
SourceConfig.removeSource(it)
}
}
oldSourceUrl = source.bookSourceUrl
@@ -207,10 +207,12 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
source.enabledExplore -> {
iv.setColorFilter(Color.GREEN)
iv.visible()
iv.contentDescription = context.getString(R.string.tag_explore_enabled)
}
else -> {
iv.setColorFilter(Color.RED)
iv.visible()
iv.contentDescription = context.getString(R.string.tag_explore_disabled)
}
}
}
@@ -5,6 +5,7 @@ import android.text.TextUtils
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookSource
import io.legado.app.help.config.SourceConfig
import io.legado.app.utils.*
import java.io.File
import java.io.FileOutputStream
@@ -38,7 +39,12 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
}
fun del(vararg sources: BookSource) {
execute { appDb.bookSourceDao.delete(*sources) }
execute {
appDb.bookSourceDao.delete(*sources)
sources.forEach {
SourceConfig.removeSource(it.bookSourceUrl)
}
}
}
fun update(vararg bookSource: BookSource) {
@@ -22,10 +22,7 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.theme.backgroundColor
import io.legado.app.lib.theme.primaryColor
import io.legado.app.ui.widget.recycler.VerticalDivider
import io.legado.app.utils.applyTint
import io.legado.app.utils.requestInputMethod
import io.legado.app.utils.setLayout
import io.legado.app.utils.splitNotBlank
import io.legado.app.utils.*
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.launch
@@ -62,7 +59,11 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
it.map { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
adapter.setItems(groups.toList())
adapter.setItems(
groups.sortedWith { o1, o2 ->
o1.cnCompare(o2)
}
)
}
}
}
@@ -45,6 +45,7 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
override fun onActivityCreated(savedInstanceState: Bundle?) {
binding.titleBar.title = intent.getStringExtra("title") ?: getString(R.string.loading)
binding.titleBar.subtitle = intent.getStringExtra("sourceName")
viewModel.initData(intent) {
val url = viewModel.baseUrl
val headerMap = viewModel.headerMap
@@ -124,12 +125,14 @@ class WebViewActivity : VMBaseActivity<ActivityWebViewBinding, WebViewModel>() {
private fun upWebViewTheme() {
if (AppConfig.isNightTheme) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDarkStrategy(
binding.webView.settings,
WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
)
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDark(
binding.webView.settings,
WebSettingsCompat.FORCE_DARK_ON
@@ -3,6 +3,7 @@ package io.legado.app.ui.config
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.InputType
import android.view.Menu
import android.view.MenuInflater
@@ -116,6 +117,14 @@ class BackupConfigFragment : PreferenceFragment(),
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
}
}
findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
it.setOnBindEditTextListener { editText ->
editText.text = AppConfig.webDavDir?.toEditable()
}
it.setOnPreferenceChangeListener { _, newValue ->
(newValue as String).isNotBlank()
}
}
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
@@ -6,7 +6,6 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import android.view.ViewConfiguration
import androidx.core.view.postDelayed
import androidx.fragment.app.activityViewModels
import androidx.preference.ListPreference
@@ -28,7 +27,9 @@ import io.legado.app.ui.widget.number.NumberPickerDialog
import io.legado.app.utils.*
import splitties.init.appCtx
/**
* 其它设置
*/
class OtherConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener {
@@ -43,7 +44,6 @@ class OtherConfigFragment : PreferenceFragment(),
AppConfig.defaultBookTreeUri = treeUri.toString()
}
}
private val slopSquare by lazy { ViewConfiguration.get(context).scaledTouchSlop }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
putPrefBoolean(PreferKey.processText, isProcessTextEnabled())
@@ -60,7 +60,6 @@ class OtherConfigFragment : PreferenceFragment(),
}
upPreferenceSummary(PreferKey.checkSource, CheckSource.summary)
upPreferenceSummary(PreferKey.bitmapCacheSize, AppConfig.bitmapCacheSize.toString())
upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -120,16 +119,7 @@ class OtherConfigFragment : PreferenceFragment(),
ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
}
}
PreferKey.pageTouchSlop -> {
NumberPickerDialog(requireContext())
.setTitle(getString(R.string.page_touch_slop_dialog_title))
.setMaxValue(9999)
.setMinValue(0)
.setValue(AppConfig.pageTouchSlop)
.show {
AppConfig.pageTouchSlop = it
}
}
}
return super.onPreferenceTreeClick(preference)
}
@@ -181,7 +171,6 @@ class OtherConfigFragment : PreferenceFragment(),
PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value)
PreferKey.webPort -> preference.summary = getString(R.string.web_port_summary, value)
PreferKey.bitmapCacheSize -> preference.summary = getString(R.string.bitmap_cache_size_summary, value)
PreferKey.pageTouchSlop -> preference.summary = getString(R.string.page_touch_slop_summary, value)
else -> if (preference is ListPreference) {
val index = preference.findIndexOfValue(value)
// Set the summary to reflect the new value.
@@ -15,6 +15,7 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.databinding.ItemFilletTextBinding
import io.legado.app.databinding.ItemFindBookBinding
import io.legado.app.help.config.SourceConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.lib.theme.accentColor
import io.legado.app.ui.login.SourceLoginActivity
@@ -174,6 +175,7 @@ class ExploreAdapter(context: Context, val callBack: CallBack) :
}
R.id.menu_del -> Coroutine.async(callBack.scope) {
appDb.bookSourceDao.delete(source)
SourceConfig.removeSource(source.bookSourceUrl)
}
}
true
@@ -63,6 +63,7 @@ class RssAdapter(context: Context, val callBack: CallBack) :
R.id.menu_top -> callBack.toTop(rssSource)
R.id.menu_edit -> callBack.edit(rssSource)
R.id.menu_del -> callBack.del(rssSource)
R.id.menu_disable -> callBack.disable(rssSource)
}
true
}
@@ -74,5 +75,6 @@ class RssAdapter(context: Context, val callBack: CallBack) :
fun toTop(rssSource: RssSource)
fun edit(rssSource: RssSource)
fun del(rssSource: RssSource)
fun disable(rssSource: RssSource)
}
}
@@ -123,7 +123,7 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
private fun initGroupData() {
groupsFlowJob?.cancel()
groupsFlowJob = launch {
appDb.rssSourceDao.flowGroup().conflate().collect {
appDb.rssSourceDao.flowGroupEnabled().conflate().collect {
groups.clear()
it.map { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
@@ -181,4 +181,8 @@ class RssFragment : VMBaseFragment<RssSourceViewModel>(R.layout.fragment_rss),
override fun del(rssSource: RssSource) {
viewModel.del(rssSource)
}
override fun disable(rssSource: RssSource) {
viewModel.disable(rssSource)
}
}
@@ -141,7 +141,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
SelectItem(getString(R.string.action_save), "save"),
SelectItem(getString(R.string.select_folder), "selectFolder")
)
) { _, charSequence, i ->
) { _, charSequence, _ ->
when (charSequence.value) {
"save" -> saveImage(webPic)
"selectFolder" -> selectSaveFolder(null)
@@ -219,12 +219,14 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
private fun upWebViewTheme() {
if (AppConfig.isNightTheme) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDarkStrategy(
binding.webView.settings,
WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
)
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
@Suppress("DEPRECATION")
WebSettingsCompat.setForceDark(
binding.webView.settings,
WebSettingsCompat.FORCE_DARK_ON
@@ -65,7 +65,11 @@ class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
it.map { group ->
groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex))
}
adapter.setItems(groups.toList())
adapter.setItems(
groups.sortedWith { o1, o2 ->
o1.cnCompare(o2)
}
)
}
}
}
@@ -50,6 +50,9 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
override val viewModel by viewModels<RssSourceViewModel>()
private val importRecordKey = "rssSourceRecordKey"
private val adapter by lazy { RssSourceAdapter(this, this) }
private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view)
}
private var sourceFlowJob: Job? = null
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
@@ -120,10 +123,21 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
R.id.menu_import_qr -> qrCodeResult.launch()
R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
R.id.menu_import_default -> viewModel.importDefault()
R.id.menu_enabled_group -> {
searchView.setQuery(getString(R.string.enabled), true)
}
R.id.menu_disabled_group -> {
searchView.setQuery(getString(R.string.disabled), true)
}
R.id.menu_group_login -> {
searchView.setQuery(getString(R.string.need_login), true)
}
R.id.menu_group_null -> {
searchView.setQuery(getString(R.string.no_group), true)
}
R.id.menu_help -> showHelp()
else -> if (item.groupId == R.id.source_group) {
binding.titleBar.findViewById<SearchView>(R.id.search_view)
.setQuery("group:${item.title}", true)
searchView.setQuery("group:${item.title}", true)
}
}
return super.onCompatOptionsItemSelected(item)
@@ -144,6 +158,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
R.id.menu_share_source -> viewModel.saveToFile(adapter.selection) {
share(it)
}
R.id.menu_check_selected_interval -> adapter.checkSelectedInterval()
}
return true
}
@@ -240,6 +255,18 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
searchKey.isNullOrBlank() -> {
appDb.rssSourceDao.flowAll()
}
searchKey == getString(R.string.enabled) -> {
appDb.rssSourceDao.flowEnabled()
}
searchKey == getString(R.string.disabled) -> {
appDb.rssSourceDao.flowDisabled()
}
searchKey == getString(R.string.need_login) -> {
appDb.rssSourceDao.flowLogin()
}
searchKey == getString(R.string.no_group) -> {
appDb.rssSourceDao.flowNoGroup()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
appDb.rssSourceDao.flowGroupSearch(key)
@@ -17,6 +17,7 @@ import io.legado.app.lib.theme.backgroundColor
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
import io.legado.app.ui.widget.recycler.ItemTouchCallback
import io.legado.app.utils.ColorUtils
import java.util.*
class RssSourceAdapter(context: Context, val callBack: CallBack) :
@@ -151,6 +152,25 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) :
callBack.upCountView()
}
fun checkSelectedInterval() {
val selectedPosition = linkedSetOf<Int>()
getItems().forEachIndexed { index, it ->
if (selected.contains(it)) {
selectedPosition.add(index)
}
}
val minPosition = Collections.min(selectedPosition)
val maxPosition = Collections.max(selectedPosition)
val itemCount = maxPosition - minPosition + 1
for (i in minPosition..maxPosition) {
getItem(i)?.let {
selected.add(it)
}
}
notifyItemRangeChanged(minPosition, itemCount, bundleOf(Pair("selected", null)))
callBack.upCountView()
}
private fun showMenu(view: View, position: Int) {
val source = getItem(position) ?: return
val popupMenu = PopupMenu(context, view)
@@ -133,4 +133,11 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application)
}
}
fun disable(rssSource: RssSource) {
execute {
rssSource.enabled = false
appDb.rssSourceDao.update(rssSource)
}
}
}
@@ -0,0 +1,15 @@
package io.legado.app.utils
import android.content.Context
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.annotation.AnimRes
import io.legado.app.help.config.AppConfig
fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
val animation = AnimationUtils.loadAnimation(context, id)
if (AppConfig.isEInkMode) {
animation.duration = 0
}
return animation
}
@@ -1,17 +0,0 @@
package io.legado.app.utils
import android.content.Context
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.annotation.AnimRes
import io.legado.app.help.config.AppConfig
object AnimationUtilsSupport {
fun loadAnimation(context: Context, @AnimRes id: Int): Animation {
val animation = AnimationUtils.loadAnimation(context, id)
if (AppConfig.isEInkMode) {
animation.duration = 0
}
return animation
}
}
@@ -6,6 +6,7 @@ import io.legado.app.constant.AppPattern.dataUriRegex
import android.icu.text.Collator
import android.icu.util.ULocale
import android.net.Uri
import android.text.Editable
import java.io.File
import java.util.*
@@ -13,6 +14,8 @@ fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
fun String.parseToUri(): Uri {
return if (isContentScheme()) {
Uri.parse(this)