Merge branch 'gedoor:master' into master
This commit is contained in:
@@ -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("<img", "< 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()
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user