Merge remote-tracking branch 'source/master'
This commit is contained in:
@@ -21,7 +21,8 @@ class ReaderProvider : ContentProvider() {
|
||||
private enum class RequestCode {
|
||||
SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
|
||||
SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
|
||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover
|
||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover,
|
||||
SaveBookProgress
|
||||
}
|
||||
|
||||
private val postBodyKey = "json"
|
||||
@@ -86,6 +87,9 @@ class ReaderProvider : ContentProvider() {
|
||||
RequestCode.SaveBook -> values?.let {
|
||||
BookController.saveBook(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveBookProgress -> values?.let {
|
||||
BookController.saveBookProgress(values.getAsString(postBodyKey))
|
||||
}
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||
)
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
package io.legado.app.api.controller
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.glide.ImageLoader
|
||||
import io.legado.app.help.storage.AppWebDav
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.model.localBook.EpubFile
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.localBook.UmdFile
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.ui.book.read.page.provider.ImageProvider
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object BookController {
|
||||
|
||||
@@ -177,7 +170,6 @@ object BookController {
|
||||
var content: String? = BookHelp.getContent(book, chapter)
|
||||
if (content != null) {
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
content = runBlocking {
|
||||
contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
@@ -190,7 +182,6 @@ object BookController {
|
||||
content = runBlocking {
|
||||
WebBook.getContentAwait(this, bookSource, book, chapter).let {
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
contentProcessor.getContent(book, chapter, it, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
}
|
||||
@@ -222,20 +213,26 @@ object BookController {
|
||||
/**
|
||||
* 保存进度
|
||||
*/
|
||||
private fun saveBookReadIndex(book: Book, index: Int) {
|
||||
book.durChapterIndex = index
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
|
||||
book.durChapterTitle = it.title
|
||||
}
|
||||
appDb.bookDao.update(book)
|
||||
AppWebDav.uploadBookProgress(book)
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = book
|
||||
ReadBook.durChapterIndex = index
|
||||
ReadBook.clearTextChapter()
|
||||
ReadBook.loadContent(true)
|
||||
}
|
||||
fun saveBookProgress(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
GSON.fromJsonObject<BookProgress>(postData)
|
||||
.onFailure { it.printOnDebug() }
|
||||
.getOrNull()?.let { bookProgress ->
|
||||
appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
|
||||
book.durChapterIndex = bookProgress.durChapterIndex
|
||||
book.durChapterPos = bookProgress.durChapterPos
|
||||
book.durChapterTitle = bookProgress.durChapterTitle
|
||||
book.durChapterTime = bookProgress.durChapterTime
|
||||
appDb.bookDao.update(book)
|
||||
AppWebDav.uploadBookProgress(bookProgress)
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = book
|
||||
ReadBook.durChapterIndex = book.durChapterIndex
|
||||
}
|
||||
return returnData.setData("")
|
||||
}
|
||||
}
|
||||
return returnData.setErrorMsg("格式不对")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,45 +245,7 @@ object BookController {
|
||||
val fileData = parameters["fileData"]?.firstOrNull()
|
||||
?: return returnData.setErrorMsg("fileData 不能为空")
|
||||
kotlin.runCatching {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
if (defaultBookTreeUri.isNullOrBlank()) return returnData.setErrorMsg("没有设置书籍保存位置!")
|
||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||
val fileBytes =
|
||||
Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
|
||||
val uri = if (treeUri.isContentScheme()) {
|
||||
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
||||
var doc = treeDoc!!.findFile(fileName)
|
||||
if (doc == null) {
|
||||
doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
|
||||
?: throw SecurityException("Permission Denial")
|
||||
}
|
||||
appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
|
||||
oStream.write(fileBytes)
|
||||
}
|
||||
doc.uri
|
||||
} else {
|
||||
val treeFile = File(treeUri.path!!)
|
||||
val file = treeFile.getFile(fileName)
|
||||
FileOutputStream(file).use { oStream ->
|
||||
oStream.write(fileBytes)
|
||||
}
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
|
||||
val book = Book(
|
||||
bookUrl = uri.toString(),
|
||||
name = nameAuthor.first,
|
||||
author = nameAuthor.second,
|
||||
originName = fileName,
|
||||
coverUrl = FileUtils.getPath(
|
||||
appCtx.externalFiles,
|
||||
"covers",
|
||||
"${MD5Utils.md5Encode16(uri.toString())}.jpg"
|
||||
)
|
||||
)
|
||||
if (book.isEpub()) EpubFile.upBookInfo(book)
|
||||
if (book.isUmd()) UmdFile.upBookInfo(book)
|
||||
appDb.bookDao.insert(book)
|
||||
LocalBook.importFileOnLine(fileData, fileName)
|
||||
}.onFailure {
|
||||
return when (it) {
|
||||
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")
|
||||
|
||||
@@ -5,11 +5,12 @@ import androidx.annotation.IntDef
|
||||
object BookType {
|
||||
const val default = 0 // 0 文本
|
||||
const val audio = 1 // 1 音频
|
||||
const val image = 2 //图片
|
||||
const val image = 2 // 2 图片
|
||||
const val file = 3 // 3 只提供下载服务的网站
|
||||
const val local = "loc_book"
|
||||
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(default, audio, image)
|
||||
@IntDef(default, audio, image, file)
|
||||
annotation class Type
|
||||
}
|
||||
@@ -28,4 +28,5 @@ object EventBus {
|
||||
const val TIP_COLOR = "tipColor"
|
||||
const val SOURCE_CHANGED = "sourceChanged"
|
||||
const val SEARCH_RESULT = "searchResult"
|
||||
const val BOOK_URL_CHANGED = "bookUrlChanged"
|
||||
}
|
||||
@@ -20,7 +20,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 48,
|
||||
version = 49,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -32,7 +32,8 @@ val appDb by lazy {
|
||||
AutoMigration(from = 44, to = 45),
|
||||
AutoMigration(from = 45, to = 46),
|
||||
AutoMigration(from = 46, to = 47),
|
||||
AutoMigration(from = 47, to = 48)
|
||||
AutoMigration(from = 47, to = 48),
|
||||
AutoMigration(from = 48, to = 49)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
@@ -21,6 +21,7 @@ interface BaseSource : JsExtensions {
|
||||
var loginUrl: String? // 登录地址
|
||||
var loginUi: String? // 登录UI
|
||||
var header: String? // 请求头
|
||||
var enabledCookieJar: Boolean? //启用cookieJar
|
||||
|
||||
fun getTag(): String
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ data class Book(
|
||||
@IgnoredOnParcel
|
||||
override var tocHtml: String? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var downloadUrls: List<String>? = null
|
||||
|
||||
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
|
||||
|
||||
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.exception.RegexTimeoutException
|
||||
@@ -15,6 +16,7 @@ import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
@@ -93,7 +95,7 @@ data class BookChapter(
|
||||
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
|
||||
}
|
||||
}
|
||||
if (useReplace && replaceRules != null) {
|
||||
if (useReplace && replaceRules != null) kotlin.run {
|
||||
replaceRules.forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
try {
|
||||
@@ -112,7 +114,10 @@ data class BookChapter(
|
||||
} catch (e: RegexTimeoutException) {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
} catch (e: CancellationException) {
|
||||
return@run
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
import java.io.InputStream
|
||||
|
||||
@Suppress("unused")
|
||||
@Parcelize
|
||||
@TypeConverters(BookSource.Converters::class)
|
||||
@Entity(
|
||||
@@ -27,7 +28,7 @@ data class BookSource(
|
||||
var bookSourceName: String = "",
|
||||
// 分组
|
||||
var bookSourceGroup: String? = null,
|
||||
// 类型,0 文本,1 音频, 2 图片
|
||||
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
|
||||
@BookType.Type
|
||||
var bookSourceType: Int = 0,
|
||||
// 详情页url正则
|
||||
@@ -38,6 +39,9 @@ data class BookSource(
|
||||
var enabled: Boolean = true,
|
||||
// 启用发现
|
||||
var enabledExplore: Boolean = true,
|
||||
// 启用okhttp CookieJAr 自动保存每次请求的cookie
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
// 并发率
|
||||
override var concurrentRate: String? = null,
|
||||
// 请求头
|
||||
@@ -158,7 +162,7 @@ data class BookSource(
|
||||
|
||||
fun removeGroup(groups: String): BookSource {
|
||||
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
|
||||
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
|
||||
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
|
||||
bookSourceGroup = TextUtils.join(",", it)
|
||||
}
|
||||
return this
|
||||
@@ -190,6 +194,7 @@ data class BookSource(
|
||||
&& equal(bookSourceComment, source.bookSourceComment)
|
||||
&& enabled == source.enabled
|
||||
&& enabledExplore == source.enabledExplore
|
||||
&& enabledCookieJar == source.enabledCookieJar
|
||||
&& equal(header, source.header)
|
||||
&& loginUrl == source.loginUrl
|
||||
&& equal(exploreUrl, source.exploreUrl)
|
||||
|
||||
@@ -24,6 +24,8 @@ data class HttpTTS(
|
||||
override var loginUrl: String? = null,
|
||||
override var loginUi: String? = null,
|
||||
override var header: String? = null,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
var loginCheckJs: String? = null,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
var lastUpdateTime: Long = System.currentTimeMillis()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -19,6 +20,8 @@ data class RssSource(
|
||||
var sourceGroup: String? = null,
|
||||
var sourceComment: String? = null,
|
||||
var enabled: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
override var concurrentRate: String? = null, //并发率
|
||||
override var header: String? = null, // 请求头
|
||||
override var loginUrl: String? = null, // 登录地址
|
||||
@@ -150,6 +153,7 @@ data class RssSource(
|
||||
style = doc.readString("$.style"),
|
||||
enableJs = doc.readBool("$.enableJs") ?: true,
|
||||
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
||||
enabledCookieJar = doc.readBool("$.enabledCookieJar") ?: false,
|
||||
customOrder = doc.readInt("$.customOrder") ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ data class BookInfoRule(
|
||||
var coverUrl: String? = null,
|
||||
var tocUrl: String? = null,
|
||||
var wordCount: String? = null,
|
||||
var canReName: String? = null
|
||||
var canReName: String? = null,
|
||||
var downloadUrls: String? = null
|
||||
) : Parcelable
|
||||
@@ -12,6 +12,7 @@ import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.utils.msg
|
||||
import io.legado.app.utils.replace
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import splitties.init.appCtx
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
@@ -135,7 +136,7 @@ class ContentProcessor private constructor(
|
||||
var mContent = content
|
||||
getContentReplaceRules().forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
kotlin.runCatching {
|
||||
try {
|
||||
mContent = if (item.isRegex) {
|
||||
mContent.replace(
|
||||
item.pattern.toRegex(),
|
||||
@@ -145,18 +146,15 @@ class ContentProcessor private constructor(
|
||||
} else {
|
||||
mContent.replace(item.pattern, item.replacement)
|
||||
}
|
||||
}.onFailure {
|
||||
when (it) {
|
||||
is RegexTimeoutException -> {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
return item.name + it.msg
|
||||
}
|
||||
else -> {
|
||||
AppLog.put("${item.name}替换出错\n${it.localizedMessage}", it)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
} catch (e: RegexTimeoutException) {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
return item.name + e.msg
|
||||
} catch (e: CancellationException) {
|
||||
return mContent
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("${item.name}替换出错\n替换内容\n${mContent}", e)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +197,10 @@ interface JsExtensions {
|
||||
*js实现读取cookie
|
||||
*/
|
||||
fun getCookie(tag: String, key: String? = null): String {
|
||||
val cookie = CookieStore.getCookie(tag)
|
||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||
return if (key != null) {
|
||||
cookieMap[key] ?: ""
|
||||
CookieStore.getKey(tag, key)
|
||||
} else {
|
||||
cookie
|
||||
CookieStore.getCookie(tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,10 +468,8 @@ interface JsExtensions {
|
||||
* @return zip指定文件的数据
|
||||
*/
|
||||
fun getZipByteArrayContent(url: String, path: String): ByteArray? {
|
||||
val bytes = if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
runBlocking {
|
||||
return@runBlocking okHttpClient.newCallResponseBody { url(url) }.bytes()
|
||||
}
|
||||
val bytes = if (url.isAbsUrl()) {
|
||||
AnalyzeUrl(url, source = getSource()).getByteArray()
|
||||
} else {
|
||||
StringUtils.hexStringToByte(url)
|
||||
}
|
||||
@@ -517,7 +513,7 @@ interface JsExtensions {
|
||||
str.isAbsUrl() -> runBlocking {
|
||||
var x = CacheManager.getByteArray(key)
|
||||
if (x == null) {
|
||||
x = okHttpClient.newCallResponseBody { url(str) }.bytes()
|
||||
x = AnalyzeUrl(str, source = getSource()).getByteArray()
|
||||
x.let {
|
||||
CacheManager.put(key, it)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.utils.*
|
||||
|
||||
object ReplaceAnalyzer {
|
||||
|
||||
fun jsonToReplaceRules(json: String): List<ReplaceRule> {
|
||||
val replaceRules = mutableListOf<ReplaceRule>()
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToReplaceRule(jsonItem.jsonString())?.let {
|
||||
if (it.isValid()) {
|
||||
replaceRules.add(it)
|
||||
fun jsonToReplaceRules(json: String): Result<MutableList<ReplaceRule>> {
|
||||
return kotlin.runCatching {
|
||||
val replaceRules = mutableListOf<ReplaceRule>()
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToReplaceRule(jsonItem.jsonString()).getOrThrow().let {
|
||||
if (it.isValid()) {
|
||||
replaceRules.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
replaceRules
|
||||
}
|
||||
return replaceRules
|
||||
}
|
||||
|
||||
private fun jsonToReplaceRule(json: String): ReplaceRule? {
|
||||
val replaceRule: ReplaceRule? = GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
||||
runCatching {
|
||||
fun jsonToReplaceRule(json: String): Result<ReplaceRule> {
|
||||
return runCatching {
|
||||
val replaceRule: ReplaceRule? =
|
||||
GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
||||
if (replaceRule == null || replaceRule.pattern.isBlank()) {
|
||||
val jsonItem = jsonPath.parse(json.trim())
|
||||
val rule = ReplaceRule()
|
||||
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
|
||||
rule.pattern = jsonItem.readString("$.regex") ?: ""
|
||||
if (rule.pattern.isEmpty()) return null
|
||||
if (rule.pattern.isEmpty()) throw NoStackTraceException("格式不对")
|
||||
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
|
||||
rule.replacement = jsonItem.readString("$.replacement") ?: ""
|
||||
rule.isRegex = jsonItem.readBool("$.isRegex") == true
|
||||
rule.scope = jsonItem.readString("$.useTo")
|
||||
rule.isEnabled = jsonItem.readBool("$.enable") == true
|
||||
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
|
||||
return rule
|
||||
return@runCatching rule
|
||||
}
|
||||
return@runCatching replaceRule
|
||||
}
|
||||
return replaceRule
|
||||
}
|
||||
|
||||
}
|
||||
@@ -150,6 +150,7 @@ object SourceAnalyzer {
|
||||
source.customOrder = sourceAny.customOrder
|
||||
source.enabled = sourceAny.enabled
|
||||
source.enabledExplore = sourceAny.enabledExplore
|
||||
source.enabledCookieJar = sourceAny.enabledCookieJar
|
||||
source.concurrentRate = sourceAny.concurrentRate
|
||||
source.header = sourceAny.header
|
||||
source.loginUrl = when (sourceAny.loginUrl) {
|
||||
@@ -219,6 +220,7 @@ object SourceAnalyzer {
|
||||
var customOrder: Int = 0, // 手动排序编号
|
||||
var enabled: Boolean = true, // 是否启用
|
||||
var enabledExplore: Boolean = true, // 启用发现
|
||||
var enabledCookieJar: Boolean = false, // 启用CookieJar
|
||||
var concurrentRate: String? = null, // 并发率
|
||||
var header: String? = null, // 请求头
|
||||
var loginUrl: Any? = null, // 登录规则
|
||||
|
||||
@@ -32,7 +32,7 @@ object SourceVerificationHelp {
|
||||
startBrowser(source, url, title, true)
|
||||
}
|
||||
|
||||
var waitUserInput: Boolean = false
|
||||
var waitUserInput = false
|
||||
while(CacheManager.get(key) == null) {
|
||||
if (!waitUserInput) {
|
||||
AppLog.putDebug("等待返回验证结果...")
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.apache.commons.text.StringEscapeUtils
|
||||
import splitties.init.appCtx
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/**
|
||||
* 后台webView
|
||||
@@ -50,14 +51,14 @@ class BackstageWebView(
|
||||
|
||||
override fun onError(error: Throwable) {
|
||||
if (!block.isCompleted)
|
||||
block.cancel(error)
|
||||
block.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
runOnUI {
|
||||
try {
|
||||
load()
|
||||
} catch (error: Throwable) {
|
||||
block.cancel(error)
|
||||
block.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ package io.legado.app.help.http
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Cookie
|
||||
import io.legado.app.help.http.api.CookieManager
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.http.api.CookieManager
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
|
||||
object CookieStore : CookieManager {
|
||||
@@ -44,7 +44,7 @@ object CookieStore : CookieManager {
|
||||
CacheManager.getFromMemory("${domain}_cookie")?.let { return it }
|
||||
val cookieBean = appDb.cookieDao.get(domain)
|
||||
val cookie = cookieBean?.cookie ?: ""
|
||||
CacheManager.putMemory(url, cookie ?: "")
|
||||
CacheManager.putMemory(url, cookie)
|
||||
return cookie
|
||||
}
|
||||
|
||||
@@ -56,8 +56,9 @@ object CookieStore : CookieManager {
|
||||
|
||||
override fun removeCookie(url: String) {
|
||||
val domain = NetworkUtils.getSubDomain(url)
|
||||
CacheManager.deleteMemory("${domain}_cookie")
|
||||
appDb.cookieDao.delete(domain)
|
||||
CacheManager.deleteMemory("${domain}_cookie")
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||
}
|
||||
|
||||
override fun cookieToMap(cookie: String): MutableMap<String, String> {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.help.http
|
||||
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.cronet.CronetInterceptor
|
||||
import io.legado.app.help.http.cronet.CronetLoader
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import okhttp3.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
@@ -23,7 +25,10 @@ val cookieJar by lazy {
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
cookies.forEach {
|
||||
CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
|
||||
//CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
|
||||
//临时保存 书源启用cookie选项再添加到数据库
|
||||
val domain = NetworkUtils.getSubDomain(url.toString())
|
||||
CacheManager.putMemory("${domain}_cookieJar", "${it.name}=${it.value}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,15 +194,28 @@ object AppWebDav {
|
||||
Coroutine.async {
|
||||
val bookProgress = BookProgress(book)
|
||||
val json = GSON.toJson(bookProgress)
|
||||
val url = getProgressUrl(book)
|
||||
val url = getProgressUrl(book.name, book.author)
|
||||
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
||||
}.onError {
|
||||
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgressUrl(book: Book): String {
|
||||
return bookProgressUrl + book.name + "_" + book.author + ".json"
|
||||
fun uploadBookProgress(bookProgress: BookProgress) {
|
||||
val authorization = authorization ?: return
|
||||
if (!syncBookProgress) return
|
||||
if (!NetworkUtils.isAvailable()) return
|
||||
Coroutine.async {
|
||||
val json = GSON.toJson(bookProgress)
|
||||
val url = getProgressUrl(bookProgress.name, bookProgress.author)
|
||||
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
||||
}.onError {
|
||||
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgressUrl(name: String, author: String): String {
|
||||
return bookProgressUrl + name + "_" + author + ".json"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +223,7 @@ object AppWebDav {
|
||||
*/
|
||||
suspend fun getBookProgress(book: Book): BookProgress? {
|
||||
authorization?.let {
|
||||
val url = getProgressUrl(book)
|
||||
val url = getProgressUrl(book.name, book.author)
|
||||
kotlin.runCatching {
|
||||
WebDav(url, it).download().let { byteArray ->
|
||||
val json = String(byteArray)
|
||||
|
||||
@@ -98,9 +98,12 @@ object ImportOldData {
|
||||
}
|
||||
|
||||
private fun importOldReplaceRule(json: String): Int {
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(json)
|
||||
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
||||
return rules.size
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(json).getOrNull()
|
||||
rules?.let {
|
||||
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
||||
return rules.size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun fromOldBooks(json: String): List<Book> {
|
||||
|
||||
@@ -22,40 +22,36 @@ import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
|
||||
object Restore {
|
||||
|
||||
suspend fun restore(context: Context, path: String) {
|
||||
withContext(IO) {
|
||||
kotlin.runCatching {
|
||||
if (path.isContentScheme()) {
|
||||
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
if (doc.name == fileName) {
|
||||
DocumentUtils.readText(context, doc.uri).let {
|
||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName")
|
||||
.writeText(it)
|
||||
if (Backup.backupFileNames.contains(doc.name)) {
|
||||
context.contentResolver.openInputStream(doc.uri)?.use { inputStream ->
|
||||
val file = File("${Backup.backupPath}${File.separator}${doc.name}")
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val file = File(path)
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
file.getFile(fileName).let {
|
||||
if (it.exists()) {
|
||||
it.copyTo(
|
||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName"),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
val dir = File(path)
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
val file = dir.getFile(fileName)
|
||||
if (file.exists()) {
|
||||
val target = File("${Backup.backupPath}${File.separator}$fileName")
|
||||
file.copyTo(target, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)
|
||||
}
|
||||
restoreDatabase()
|
||||
restoreConfig()
|
||||
@@ -129,7 +125,7 @@ object Restore {
|
||||
ThemeConfig.upConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复主题出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
if (!BackupConfig.ignoreReadConfig) {
|
||||
//恢复阅读界面配置
|
||||
@@ -142,7 +138,7 @@ object Restore {
|
||||
ReadBookConfig.initConfigs()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
try {
|
||||
val file =
|
||||
@@ -153,7 +149,7 @@ object Restore {
|
||||
ReadBookConfig.initShareConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
@@ -118,7 +118,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
val v = io.legado.app.ui.widget.prefs.Preference.bindView<ColorPanelView>(
|
||||
val v = io.legado.app.lib.prefs.Preference.bindView<ColorPanelView>(
|
||||
context, holder, icon, title, summary, widgetLayoutResource,
|
||||
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.preference.EditTextPreference.OnBindEditTextListener
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import io.legado.app.R
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.utils.applyTint
|
||||
|
||||
class EditTextPreference(context: Context, attrs: AttributeSet) :
|
||||
androidx.preference.EditTextPreference(context, attrs) {
|
||||
|
||||
private var mOnBindEditTextListener: OnBindEditTextListener? = null
|
||||
private val onBindEditTextListener = OnBindEditTextListener { editText ->
|
||||
editText.applyTint(context.accentColor)
|
||||
mOnBindEditTextListener?.onBindEditText(editText)
|
||||
}
|
||||
|
||||
init {
|
||||
// isPersistent = true
|
||||
layoutResource = R.layout.view_preference
|
||||
super.setOnBindEditTextListener(onBindEditTextListener)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
|
||||
super.onBindViewHolder(holder)
|
||||
}
|
||||
|
||||
override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) {
|
||||
mOnBindEditTextListener = onBindEditTextListener
|
||||
}
|
||||
|
||||
}
|
||||
+10
-1
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
||||
@@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||
dialog.window?.decorView?.post {
|
||||
(dialog as AlertDialog).run {
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
+10
-1
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.ListPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
||||
@@ -23,6 +25,13 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||
dialog.window?.decorView?.post {
|
||||
(dialog as AlertDialog).run {
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
+10
-1
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
|
||||
@@ -25,6 +27,13 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||
dialog.window?.decorView?.post {
|
||||
(dialog as AlertDialog).run {
|
||||
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
+3
-9
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
@@ -25,14 +25,8 @@ class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
val v = Preference.bindView<TextView>(
|
||||
context,
|
||||
holder,
|
||||
icon,
|
||||
title,
|
||||
summary,
|
||||
widgetLayoutResource,
|
||||
R.id.text_view,
|
||||
isBottomBackground = isBottomBackground
|
||||
context, holder, icon, title, summary, widgetLayoutResource,
|
||||
R.id.text_view, isBottomBackground = isBottomBackground
|
||||
)
|
||||
if (v is TextView) {
|
||||
v.text = entry
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
package io.legado.app.lib.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
+8
-9
@@ -1,13 +1,13 @@
|
||||
package io.legado.app.base
|
||||
package io.legado.app.lib.prefs.fragment
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.*
|
||||
import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.ListPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
|
||||
import io.legado.app.lib.prefs.EditTextPreferenceDialog
|
||||
import io.legado.app.lib.prefs.ListPreferenceDialog
|
||||
import io.legado.app.lib.prefs.MultiSelectListPreferenceDialog
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
abstract class PreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
|
||||
|
||||
@@ -34,7 +34,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
return
|
||||
}
|
||||
|
||||
val f: DialogFragment = when (preference) {
|
||||
val dialogFragment: DialogFragment = when (preference) {
|
||||
is EditTextPreference -> {
|
||||
EditTextPreferenceDialog.newInstance(preference.getKey())
|
||||
}
|
||||
@@ -54,10 +54,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
f.setTargetFragment(this, 0)
|
||||
dialogFragment.setTargetFragment(this, 0)
|
||||
|
||||
f.show(parentFragmentManager, dialogFragmentTag)
|
||||
dialogFragment.show(parentFragmentManager, dialogFragmentTag)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -150,6 +150,7 @@ object BookCover {
|
||||
override var loginUrl: String? = null,
|
||||
override var loginUi: String? = null,
|
||||
override var header: String? = null,
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
) : BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
|
||||
@@ -131,9 +131,9 @@ object CacheBook {
|
||||
|
||||
class CacheBookModel(var bookSource: BookSource, var book: Book) {
|
||||
|
||||
private val waitDownloadSet = hashSetOf<Int>()
|
||||
private val onDownloadSet = hashSetOf<Int>()
|
||||
private val successDownloadSet = hashSetOf<Int>()
|
||||
private val waitDownloadSet = linkedSetOf<Int>()
|
||||
private val onDownloadSet = linkedSetOf<Int>()
|
||||
private val successDownloadSet = linkedSetOf<Int>()
|
||||
private val errorDownloadMap = hashMapOf<Int, Int>()
|
||||
|
||||
val waitCount get() = waitDownloadSet.size
|
||||
@@ -174,7 +174,7 @@ object CacheBook {
|
||||
}
|
||||
onDownloadSet.remove(index)
|
||||
//重试3次
|
||||
if (errorDownloadMap[index] ?: 0 < 3) {
|
||||
if ((errorDownloadMap[index] ?: 0) < 3) {
|
||||
waitDownloadSet.add(index)
|
||||
} else {
|
||||
AppLog.put(
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.*
|
||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||
import io.legado.app.model.rss.Rss
|
||||
@@ -238,7 +239,11 @@ object Debug {
|
||||
.onSuccess {
|
||||
log(debugSource, "︽详情页解析完成")
|
||||
log(debugSource, showTime = false)
|
||||
tocDebug(scope, bookSource, book)
|
||||
if (book.type != BookType.file) {
|
||||
tocDebug(scope, bookSource, book)
|
||||
} else {
|
||||
log(debugSource, "≡文件类书源跳过解析目录", state = 1000)
|
||||
}
|
||||
}
|
||||
.onError {
|
||||
log(debugSource, it.msg, state = -1)
|
||||
|
||||
@@ -69,6 +69,7 @@ class AnalyzeUrl(
|
||||
private var retry: Int = 0
|
||||
private var useWebView: Boolean = false
|
||||
private var webJs: String? = null
|
||||
private val enabledCookieJar = source?.enabledCookieJar ?: false
|
||||
|
||||
init {
|
||||
if (!mUrl.isDataUrl()) {
|
||||
@@ -519,17 +520,26 @@ class AnalyzeUrl(
|
||||
}
|
||||
|
||||
/**
|
||||
*设置cookie urlOption的优先级大于书源保存的cookie
|
||||
*设置cookie 优先级
|
||||
* urlOption临时cookie > 数据库cookie = okhttp CookieJar保存在内存中的cookie
|
||||
*@param tag 书源url 缺省为传入的url
|
||||
*/
|
||||
private fun setCookie(tag: String?) {
|
||||
val cookie = CookieStore.getCookie(tag ?: url)
|
||||
val domain = NetworkUtils.getSubDomain(tag ?: url)
|
||||
//书源启用保存cookie时 添加内存中的cookie到数据库
|
||||
if (enabledCookieJar) {
|
||||
val key = "${domain}_cookieJar"
|
||||
CacheManager.getFromMemory(key)?.let {
|
||||
CookieStore.replaceCookie(domain, it)
|
||||
CacheManager.deleteMemory(key)
|
||||
}
|
||||
}
|
||||
val cookie = CookieStore.getCookie(domain)
|
||||
if (cookie.isNotEmpty()) {
|
||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
|
||||
cookieMap.putAll(customCookieMap)
|
||||
val newCookie = CookieStore.mapToCookie(cookieMap)
|
||||
newCookie?.let {
|
||||
CookieStore.mapToCookie(cookieMap)?.let {
|
||||
headerMap.put("Cookie", it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
package io.legado.app.model.localBook
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.script.SimpleBindings
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.exception.TocEmptyException
|
||||
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 splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* 书籍文件导入 目录正文解析
|
||||
* 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
|
||||
*/
|
||||
object LocalBook {
|
||||
|
||||
private val nameAuthorPatterns = arrayOf(
|
||||
@@ -41,6 +46,20 @@ object LocalBook {
|
||||
throw FileNotFoundException("${uri.path} 文件不存在")
|
||||
}
|
||||
|
||||
fun getLastModified(book: Book): Result<Long> {
|
||||
return kotlin.runCatching {
|
||||
val uri = Uri.parse(book.bookUrl)
|
||||
if (uri.isContentScheme()) {
|
||||
return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified()
|
||||
}
|
||||
val file = File(uri.path!!)
|
||||
if (file.exists()) {
|
||||
return@runCatching File(uri.path!!).lastModified()
|
||||
}
|
||||
throw FileNotFoundException("${uri.path} 文件不存在")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getChapterList(book: Book): ArrayList<BookChapter> {
|
||||
val chapters = when {
|
||||
@@ -79,6 +98,23 @@ object LocalBook {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载在线的文件并自动导入到阅读(txt umd epub)
|
||||
* 压缩文件请先提示用户解压
|
||||
*/
|
||||
fun importFileOnLine(
|
||||
str: String,
|
||||
fileName: String,
|
||||
source: BaseSource? = null,
|
||||
): Book {
|
||||
return saveBookFile(str, fileName, source).let {
|
||||
importFile(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入本地文件
|
||||
*/
|
||||
fun importFile(uri: Uri): Book {
|
||||
val bookUrl: String
|
||||
val updateTime: Long
|
||||
@@ -119,7 +155,10 @@ object LocalBook {
|
||||
return book
|
||||
}
|
||||
|
||||
fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
||||
/**
|
||||
* 从文件分析书籍必要信息(书名 作者等)
|
||||
*/
|
||||
private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
||||
val tempFileName = fileName.substringBeforeLast(".")
|
||||
var name: String
|
||||
var author: String
|
||||
@@ -171,4 +210,75 @@ object LocalBook {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载在线的文件
|
||||
*/
|
||||
fun saveBookFile(
|
||||
str: String,
|
||||
fileName: String,
|
||||
source: BaseSource? = null,
|
||||
): Uri {
|
||||
val bytes = when {
|
||||
str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
|
||||
str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
|
||||
else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
|
||||
}
|
||||
return saveBookFile(bytes, fileName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析下载文件类书源的下载链接的文件后缀
|
||||
* https://www.example.com/download/{fileName}.{type} 含有文件名和后缀
|
||||
* https://www.example.com/download/?fileid=1234, {type: "txt"} 规则设置
|
||||
*/
|
||||
fun parseFileSuffix(url: String): String {
|
||||
val analyzeUrl = AnalyzeUrl(url)
|
||||
val urlNoOption = analyzeUrl.url
|
||||
val lastPath = urlNoOption.substringAfterLast("/")
|
||||
val fileType = lastPath.substringAfterLast(".")
|
||||
val type = analyzeUrl.type
|
||||
return type ?: fileType
|
||||
}
|
||||
|
||||
private fun saveBookFile(
|
||||
bytes: ByteArray,
|
||||
fileName: String
|
||||
): Uri {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
|
||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||
return if (treeUri.isContentScheme()) {
|
||||
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
||||
var doc = treeDoc!!.findFile(fileName)
|
||||
if (doc == null) {
|
||||
doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
|
||||
?: throw SecurityException("Permission Denial")
|
||||
}
|
||||
appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
|
||||
oStream.write(bytes)
|
||||
}
|
||||
doc.uri
|
||||
} else {
|
||||
val treeFile = File(treeUri.path!!)
|
||||
val file = treeFile.getFile(fileName)
|
||||
FileOutputStream(file).use { oStream ->
|
||||
oStream.write(bytes)
|
||||
}
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
//文件类书源 合并在线书籍信息 在线 > 本地
|
||||
fun mergeBook(localBook: Book, onLineBook: Book?): Book {
|
||||
onLineBook ?: return localBook
|
||||
localBook.name = onLineBook.name.ifBlank { localBook.name }
|
||||
localBook.author = onLineBook.author.ifBlank { localBook.author }
|
||||
localBook.coverUrl = onLineBook.coverUrl
|
||||
localBook.intro =
|
||||
if (onLineBook.intro.isNullOrBlank()) localBook.intro else onLineBook.intro
|
||||
localBook.save()
|
||||
return localBook
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 本地书籍解析
|
||||
# 书籍文件导入解析
|
||||
|
||||
* BaseLocalBookParse.kt 本地书籍解析接口
|
||||
* LocalBook.kt 总入口
|
||||
* LocalBook.kt 导入解析总入口
|
||||
* TextFile.kt 解析txt
|
||||
* EpubFile.kt 解析epub
|
||||
* UmdFile.kt 解析umd
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.legado.app.model.webBook
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -137,14 +139,29 @@ object BookInfo {
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取封面出错", e)
|
||||
}
|
||||
scope.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||
if (book.tocUrl == baseUrl) {
|
||||
book.tocHtml = body
|
||||
if (book.type != BookType.file) {
|
||||
scope.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||
if (book.tocUrl == baseUrl) {
|
||||
book.tocHtml = body
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
|
||||
} else {
|
||||
scope.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接")
|
||||
book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true)
|
||||
if (book.downloadUrls == null) {
|
||||
Debug.log(bookSource.bookSourceUrl, "└")
|
||||
throw NoStackTraceException("下载链接为空")
|
||||
} else {
|
||||
Debug.log(
|
||||
bookSource.bookSourceUrl,
|
||||
"└" + TextUtils.join(",\n", book.downloadUrls!!)
|
||||
)
|
||||
}
|
||||
}
|
||||
Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.script.ScriptException
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseService
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.IntentAction
|
||||
import io.legado.app.data.appDb
|
||||
@@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
|
||||
mBook = WebBook.getBookInfoAwait(this, source, mBook)
|
||||
}
|
||||
//校验目录
|
||||
if (CheckSource.checkCategory) {
|
||||
if (CheckSource.checkCategory &&
|
||||
source.bookSourceType != BookType.file
|
||||
) {
|
||||
val toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
|
||||
val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
|
||||
//校验正文
|
||||
|
||||
@@ -38,7 +38,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
private val ttsFolderPath: String by lazy {
|
||||
cacheDir.absolutePath + File.separator + "httpTTS" + File.separator
|
||||
}
|
||||
private var speechRate: Int = AppConfig.speechRatePlay
|
||||
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
||||
private var downloadTask: Coroutine<*>? = null
|
||||
private var playIndexJob: Job? = null
|
||||
private var downloadTaskIsActive = false
|
||||
@@ -301,7 +301,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
override fun upSpeechRate(reset: Boolean) {
|
||||
downloadTask?.cancel()
|
||||
exoPlayer.stop()
|
||||
speechRate = AppConfig.speechRatePlay
|
||||
speechRate = AppConfig.speechRatePlay + 5
|
||||
downloadAudio()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,7 @@ package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.isJsonArray
|
||||
|
||||
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
@@ -37,47 +28,4 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
|
||||
}
|
||||
}
|
||||
|
||||
fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
if (json.isJsonArray()) {
|
||||
GSON.fromJsonArray<TxtTocRule>(json).getOrThrow()?.let {
|
||||
appDb.txtTocRuleDao.insert(*it.toTypedArray())
|
||||
} ?: throw NoStackTraceException("格式不对")
|
||||
} else {
|
||||
GSON.fromJsonObject<TxtTocRule>(json).getOrThrow()?.let {
|
||||
appDb.txtTocRuleDao.insert(it)
|
||||
} ?: throw NoStackTraceException("格式不对")
|
||||
}
|
||||
}.onSuccess {
|
||||
finally.invoke(context.getString(R.string.success), "导入Txt规则成功")
|
||||
}.onError {
|
||||
finally.invoke(
|
||||
context.getString(R.string.error),
|
||||
it.localizedMessage ?: context.getString(R.string.unknown_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
if (json.isJsonArray()) {
|
||||
GSON.fromJsonArray<ThemeConfig.Config>(json).getOrThrow()?.forEach {
|
||||
ThemeConfig.addConfig(it)
|
||||
}
|
||||
} else {
|
||||
GSON.fromJsonObject<ThemeConfig.Config>(json).getOrThrow()?.let {
|
||||
ThemeConfig.addConfig(it)
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
finally.invoke(context.getString(R.string.success), "导入主题成功")
|
||||
}.onError {
|
||||
finally.invoke(
|
||||
context.getString(R.string.error),
|
||||
it.localizedMessage ?: context.getString(R.string.unknown_error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -61,21 +61,7 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
fun importSource(text: String) {
|
||||
execute {
|
||||
val mText = text.trim()
|
||||
when {
|
||||
mText.isJsonObject() -> {
|
||||
HttpTTS.fromJson(mText).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
mText.isJsonArray() -> HttpTTS.fromJsonArray(mText).getOrThrow().let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
mText.isAbsUrl() -> {
|
||||
importSourceUrl(mText)
|
||||
}
|
||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
importSourceAwait(text.trim())
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||
@@ -84,11 +70,28 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceAwait(text: String) {
|
||||
when {
|
||||
text.isJsonObject() -> {
|
||||
HttpTTS.fromJson(text).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow().let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
text.isAbsUrl() -> {
|
||||
importSourceUrl(text)
|
||||
}
|
||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importSourceUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text().let {
|
||||
allSources.addAll(HttpTTS.fromJsonArray(it).getOrThrow())
|
||||
importSourceAwait(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.net.Uri
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseDialogFragment
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
||||
import io.legado.app.databinding.ItemBookFileImportBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
|
||||
/**
|
||||
* 导入在线书籍文件弹出窗口
|
||||
*/
|
||||
class ImportOnLineBookFileDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) {
|
||||
|
||||
|
||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||
private val viewModel by viewModels<ImportOnLineBookFileViewModel>()
|
||||
private val adapter by lazy { BookFileAdapter(requireContext()) }
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val bookUrl = arguments?.getString("bookUrl")
|
||||
viewModel.initData(bookUrl)
|
||||
binding.toolBar.setBackgroundColor(primaryColor)
|
||||
binding.toolBar.setTitle(R.string.download_and_import_file)
|
||||
binding.rotateLoading.show()
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allBookFiles)
|
||||
}
|
||||
}
|
||||
viewModel.savedFileUriData.observe(this) {
|
||||
requireContext().openFileUri(it, "*/*")
|
||||
}
|
||||
}
|
||||
|
||||
private fun importFileAndUpdate(url: String, fileName: String) {
|
||||
val waitDialog = WaitDialog(requireContext())
|
||||
waitDialog.show()
|
||||
viewModel.importOnLineBookFile(url, fileName) {
|
||||
waitDialog.dismiss()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadFile(url: String, fileName: String) {
|
||||
val waitDialog = WaitDialog(requireContext())
|
||||
waitDialog.show()
|
||||
viewModel.downloadUrl(url, fileName) {
|
||||
waitDialog.dismiss()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
inner class BookFileAdapter(context: Context) :
|
||||
RecyclerAdapter<Triple<String, String, Boolean>
|
||||
, ItemBookFileImportBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding {
|
||||
return ItemBookFileImportBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemBookFileImportBinding,
|
||||
item: Triple<String, String, Boolean>,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.apply {
|
||||
cbFileName.text = item.second
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemBookFileImportBinding
|
||||
) {
|
||||
binding.apply {
|
||||
cbFileName.setOnClickListener {
|
||||
val selectFile = viewModel.allBookFiles[holder.layoutPosition]
|
||||
if (selectFile.third) {
|
||||
importFileAndUpdate(selectFile.first, selectFile.second)
|
||||
} else {
|
||||
alert(
|
||||
title = getString(R.string.draw),
|
||||
message = getString(R.string.file_not_supported, selectFile.second)
|
||||
) {
|
||||
okButton {
|
||||
importFileAndUpdate(selectFile.first, selectFile.second)
|
||||
}
|
||||
neutralButton(R.string.open_fun) {
|
||||
downloadFile(selectFile.first, selectFile.second)
|
||||
}
|
||||
cancelButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.utils.*
|
||||
|
||||
class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
val allBookFiles = arrayListOf<Triple<String, String, Boolean>>()
|
||||
val errorLiveData = MutableLiveData<String>()
|
||||
val successLiveData = MutableLiveData<Int>()
|
||||
val savedFileUriData = MutableLiveData<Uri>()
|
||||
var bookSource: BookSource? = null
|
||||
|
||||
fun initData(bookUrl: String?) {
|
||||
execute {
|
||||
bookUrl ?: throw NoStackTraceException("书籍详情页链接为空")
|
||||
val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
|
||||
?: throw NoStackTraceException("book is null")
|
||||
bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw NoStackTraceException("bookSource is null")
|
||||
val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls
|
||||
val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body
|
||||
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||
analyzeRule.setContent(content).setBaseUrl(bookUrl)
|
||||
val fileName = "${book.name} 作者:${book.author}"
|
||||
analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let {
|
||||
it.forEach { url ->
|
||||
val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}"
|
||||
val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
|
||||
allBookFiles.add(Triple(url, mFileName, isSupportedFile))
|
||||
}
|
||||
} ?: throw NoStackTraceException("下载链接规则解析为空")
|
||||
}.onSuccess {
|
||||
successLiveData.postValue(allBookFiles.size)
|
||||
}.onError {
|
||||
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||
context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun downloadUrl(url: String, fileName: String, success: () -> Unit) {
|
||||
execute {
|
||||
LocalBook.saveBookFile(url, fileName, bookSource).let {
|
||||
savedFileUriData.postValue(it)
|
||||
}
|
||||
}.onSuccess {
|
||||
success.invoke()
|
||||
}.onError {
|
||||
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) {
|
||||
execute {
|
||||
LocalBook.importFileOnLine(url, fileName, bookSource).let {
|
||||
postEvent(EventBus.BOOK_URL_CHANGED, it.bookUrl)
|
||||
}
|
||||
}.onSuccess {
|
||||
success.invoke()
|
||||
}.onError {
|
||||
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -88,14 +88,14 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
|
||||
adapter.notifyDataSetChanged()
|
||||
upSelectText()
|
||||
}
|
||||
viewModel.errorLiveData.observe(this, {
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
})
|
||||
viewModel.successLiveData.observe(this, {
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allRules)
|
||||
@@ -106,7 +106,7 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
|
||||
visible()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
val source = arguments?.getString("source")
|
||||
if (source.isNullOrEmpty()) {
|
||||
dismiss()
|
||||
|
||||
@@ -6,12 +6,15 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.ReplaceAnalyzer
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.isAbsUrl
|
||||
import io.legado.app.utils.isJsonArray
|
||||
import io.legado.app.utils.isJsonObject
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
|
||||
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
@@ -83,17 +86,7 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
fun import(text: String) {
|
||||
execute {
|
||||
if (text.isAbsUrl()) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(text)
|
||||
}.text("utf-8").let {
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(it)
|
||||
allRules.addAll(rules)
|
||||
}
|
||||
} else {
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(text)
|
||||
allRules.addAll(rules)
|
||||
}
|
||||
importAwait(text.trim())
|
||||
}.onError {
|
||||
errorLiveData.postValue(it.localizedMessage ?: "ERROR")
|
||||
}.onSuccess {
|
||||
@@ -101,6 +94,29 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importAwait(text: String) {
|
||||
when {
|
||||
text.isAbsUrl() -> importUrl(text)
|
||||
text.isJsonArray() -> {
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(text).getOrThrow()
|
||||
allRules.addAll(rules)
|
||||
}
|
||||
text.isJsonObject() -> {
|
||||
val rule = ReplaceAnalyzer.jsonToReplaceRule(text).getOrThrow()
|
||||
allRules.add(rule)
|
||||
}
|
||||
else -> throw NoStackTraceException("格式不对")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importUrl(url: String) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.text("utf-8").let {
|
||||
importAwait(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun comparisonSource() {
|
||||
execute {
|
||||
allRules.forEach {
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.activity.viewModels
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
@@ -26,6 +27,7 @@ import io.legado.app.lib.theme.bottomBackground
|
||||
import io.legado.app.lib.theme.getPrimaryTextColor
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import io.legado.app.ui.association.ImportOnLineBookFileDialog
|
||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
@@ -245,7 +247,7 @@ class BookInfoActivity :
|
||||
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
|
||||
}
|
||||
chapterList.isNullOrEmpty() -> {
|
||||
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.error_load_toc))
|
||||
binding.tvToc.text = if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString(R.string.toc_s, getString(R.string.error_load_toc))
|
||||
}
|
||||
else -> {
|
||||
viewModel.bookData.value?.let {
|
||||
@@ -293,8 +295,14 @@ class BookInfoActivity :
|
||||
true
|
||||
}
|
||||
tvRead.setOnClickListener {
|
||||
viewModel.bookData.value?.let {
|
||||
readBook(it)
|
||||
viewModel.bookData.value?.let { book ->
|
||||
if (viewModel.isImportBookOnLine) {
|
||||
showDialogFragment<ImportOnLineBookFileDialog> {
|
||||
putString("bookUrl", book.bookUrl)
|
||||
}
|
||||
} else {
|
||||
readBook(book)
|
||||
}
|
||||
} ?: toastOnUi("Book is null")
|
||||
}
|
||||
tvShelf.setOnClickListener {
|
||||
@@ -497,4 +505,9 @@ class BookInfoActivity :
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeLiveBus() {
|
||||
observeEvent<String>(EventBus.BOOK_URL_CHANGED) {
|
||||
viewModel.changeToLocalBook(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package io.legado.app.ui.book.info
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
@@ -30,6 +32,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
var inBookshelf = false
|
||||
var bookSource: BookSource? = null
|
||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||
var isImportBookOnLine = false
|
||||
|
||||
fun initData(intent: Intent) {
|
||||
execute {
|
||||
@@ -73,8 +76,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
upCoverByRule(book)
|
||||
bookSource = if (book.isLocalBook()) null else
|
||||
appDb.bookSourceDao.getBookSource(book.origin)
|
||||
isImportBookOnLine = (bookSource?.bookSourceType ?: BookType.local) == BookType.file
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else if (isImportBookOnLine) {
|
||||
chapterListData.postValue(emptyList())
|
||||
} else {
|
||||
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
||||
if (chapterList.isNotEmpty()) {
|
||||
@@ -113,6 +119,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
|
||||
.onSuccess(IO) {
|
||||
bookData.postValue(book)
|
||||
if (isImportBookOnLine) {
|
||||
appDb.searchBookDao.update(book.toSearchBook())
|
||||
}
|
||||
if (inBookshelf) {
|
||||
appDb.bookDao.update(book)
|
||||
}
|
||||
@@ -141,6 +150,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
appDb.bookChapterDao.insert(*it.toTypedArray())
|
||||
chapterListData.postValue(it)
|
||||
}
|
||||
} else if (isImportBookOnLine) {
|
||||
chapterListData.postValue(emptyList())
|
||||
} else {
|
||||
bookSource?.let { bookSource ->
|
||||
WebBook.getChapterList(this, bookSource, book)
|
||||
@@ -282,4 +293,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeToLocalBook(bookUrl: String) {
|
||||
appDb.bookDao.getBook(bookUrl)?.let { localBook ->
|
||||
isImportBookOnLine = false
|
||||
inBookshelf = true
|
||||
LocalBook.mergeBook(localBook, bookData.value).let {
|
||||
bookData.postValue(it)
|
||||
loadChapter(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -229,8 +229,12 @@ abstract class BaseReadBookActivity :
|
||||
customView { alertBinding.root }
|
||||
yesButton {
|
||||
alertBinding.run {
|
||||
val start = editStart.text?.toString()?.toInt() ?: 0
|
||||
val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum
|
||||
val start = editStart.text!!.toString().let {
|
||||
if (it.isEmpty()) 0 else it.toInt()
|
||||
}
|
||||
val end = editEnd.text!!.toString().let {
|
||||
if (it.isEmpty()) book.totalChapterNum else it.toInt()
|
||||
}
|
||||
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,41 +61,33 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
}
|
||||
|
||||
private fun initBook(book: Book) {
|
||||
if (ReadBook.book?.bookUrl != book.bookUrl) {
|
||||
ReadBook.resetData(book)
|
||||
isInitFinish = true
|
||||
if (ReadBook.chapterSize == 0) {
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else {
|
||||
loadChapterList(book)
|
||||
}
|
||||
val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
|
||||
if (isSameBook) ReadBook.upData(book) else ReadBook.resetData(book)
|
||||
isInitFinish = true
|
||||
if (ReadBook.chapterSize == 0) {
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else {
|
||||
loadChapterList(book)
|
||||
}
|
||||
} else if (book.isLocalBook()
|
||||
&& LocalBook.getLastModified(book).getOrDefault(0L) > book.latestChapterTime
|
||||
) {
|
||||
loadChapterList(book)
|
||||
} else if (isSameBook) {
|
||||
if (ReadBook.curTextChapter != null) {
|
||||
ReadBook.callBack?.upContent(resetPageOffset = false)
|
||||
} else {
|
||||
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
||||
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
||||
}
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
}
|
||||
syncBookProgress(book)
|
||||
} else {
|
||||
ReadBook.upData(book)
|
||||
isInitFinish = true
|
||||
if (ReadBook.chapterSize == 0) {
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else {
|
||||
loadChapterList(book)
|
||||
}
|
||||
} else {
|
||||
if (ReadBook.curTextChapter != null) {
|
||||
ReadBook.callBack?.upContent(resetPageOffset = false)
|
||||
} else {
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
}
|
||||
}
|
||||
if (!BaseReadAloudService.isRun) {
|
||||
syncBookProgress(book)
|
||||
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
||||
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
||||
}
|
||||
ReadBook.loadContent(resetPageOffset = isSameBook)
|
||||
}
|
||||
if (!isSameBook || !BaseReadAloudService.isRun) {
|
||||
syncBookProgress(book)
|
||||
}
|
||||
if (!book.isLocalBook() && ReadBook.bookSource == null) {
|
||||
autoChangeSource(book.name, book.author)
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.widget.LinearLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.bottomBackground
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.model.ReadBook
|
||||
@@ -67,7 +67,7 @@ class MoreConfigDialog : DialogFragment() {
|
||||
(activity as ReadBookActivity).bottomDialog--
|
||||
}
|
||||
|
||||
class ReadPreferenceFragment : BasePreferenceFragment(),
|
||||
class ReadPreferenceFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
|
||||
@@ -10,12 +10,12 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.IntentHelp
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.backgroundColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.model.ReadAloud
|
||||
@@ -54,7 +54,7 @@ class ReadAloudConfigDialog : DialogFragment() {
|
||||
.commit()
|
||||
}
|
||||
|
||||
class ReadAloudPreferenceFragment : BasePreferenceFragment(),
|
||||
class ReadAloudPreferenceFragment : PreferenceFragment(),
|
||||
SpeakEngineDialog.CallBack,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.legado.app.databinding.ActivityBookSourceEditBinding
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.backgroundColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
|
||||
@@ -47,7 +48,7 @@ class BookSourceEditActivity :
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it ?: return@registerForActivityResult
|
||||
viewModel.importSource(it) { source ->
|
||||
upRecyclerView(source)
|
||||
upSourceView(source)
|
||||
}
|
||||
}
|
||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||
@@ -68,7 +69,7 @@ class BookSourceEditActivity :
|
||||
softKeyboardTool.attachToWindow(window)
|
||||
initView()
|
||||
viewModel.initData(intent) {
|
||||
upRecyclerView()
|
||||
upSourceView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,9 +110,10 @@ class BookSourceEditActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.menu_clear_cookie -> viewModel.clearCookie(getSource().bookSourceUrl)
|
||||
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
||||
R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
|
||||
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
|
||||
R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
|
||||
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
||||
R.id.menu_share_str -> share(GSON.toJson(getSource()))
|
||||
R.id.menu_share_qr -> shareWithQr(
|
||||
@@ -139,6 +141,7 @@ class BookSourceEditActivity :
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.tabLayout.setBackgroundColor(backgroundColor)
|
||||
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
|
||||
@@ -186,12 +189,14 @@ class BookSourceEditActivity :
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun upRecyclerView(source: BookSource? = viewModel.bookSource) {
|
||||
private fun upSourceView(source: BookSource? = viewModel.bookSource) {
|
||||
source?.let {
|
||||
binding.cbIsEnable.isChecked = it.enabled
|
||||
binding.cbIsEnableFind.isChecked = it.enabledExplore
|
||||
binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
|
||||
binding.spType.setSelection(
|
||||
when (it.bookSourceType) {
|
||||
BookType.file -> 3
|
||||
BookType.image -> 2
|
||||
BookType.audio -> 1
|
||||
else -> 0
|
||||
@@ -261,6 +266,7 @@ class BookSourceEditActivity :
|
||||
add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
|
||||
add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
|
||||
add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
|
||||
add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
|
||||
}
|
||||
//目录页
|
||||
val tr = source?.getTocRule()
|
||||
@@ -295,7 +301,9 @@ class BookSourceEditActivity :
|
||||
val source = viewModel.bookSource?.copy() ?: BookSource()
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.enabledExplore = binding.cbIsEnableFind.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
source.bookSourceType = when (binding.spType.selectedItemPosition) {
|
||||
3 -> BookType.file
|
||||
2 -> BookType.image
|
||||
1 -> BookType.audio
|
||||
else -> BookType.default
|
||||
@@ -389,6 +397,7 @@ class BookSourceEditActivity :
|
||||
"tocUrl" -> bookInfoRule.tocUrl =
|
||||
viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
|
||||
"canReName" -> bookInfoRule.canReName = it.value
|
||||
"downloadUrls" -> bookInfoRule.downloadUrls = viewModel.ruleComplete(it.value, bookInfoRule.init)
|
||||
}
|
||||
}
|
||||
tocEntities.forEach {
|
||||
|
||||
@@ -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.http.CookieStore
|
||||
import io.legado.app.help.http.newCallStrResponse
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.utils.*
|
||||
@@ -95,6 +96,12 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookie(url: String) {
|
||||
execute {
|
||||
CookieStore.removeCookie(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
||||
if (autoComplete) {
|
||||
return RuleComplete.autoComplete(rule, preRule, type)
|
||||
|
||||
@@ -19,8 +19,10 @@ import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.longToastOnUi
|
||||
import io.legado.app.utils.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
@@ -54,17 +56,10 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
|
||||
}
|
||||
|
||||
private val replaceRules
|
||||
get() = callback.book?.let {
|
||||
ContentProcessor.get(it.name, it.origin).getTitleReplaceRules()
|
||||
}
|
||||
private val useReplace
|
||||
get() = AppConfig.tocUiUseReplace && callback.book?.getUseReplaceRule() == true
|
||||
private var upDisplayTileJob: Coroutine<*>? = null
|
||||
|
||||
override fun onCurrentListChanged() {
|
||||
super.onCurrentListChanged()
|
||||
upDisplayTitle()
|
||||
callback.onListChanged()
|
||||
}
|
||||
|
||||
@@ -73,38 +68,52 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
displayTitleMap.clear()
|
||||
}
|
||||
|
||||
fun upDisplayTitle() {
|
||||
fun upDisplayTitles(startIndex: Int) {
|
||||
upDisplayTileJob?.cancel()
|
||||
upDisplayTileJob = Coroutine.async(callback.scope) {
|
||||
val replaceRules = replaceRules
|
||||
val useReplace = useReplace
|
||||
getItems().forEach {
|
||||
if (!isActive) {
|
||||
return@async
|
||||
val book = callback.book ?: return@async
|
||||
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||
val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
|
||||
val items = getItems()
|
||||
async {
|
||||
for (i in startIndex until items.size) {
|
||||
val item = items[i]
|
||||
if (displayTitleMap[item.title] == null) {
|
||||
ensureActive()
|
||||
val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
|
||||
ensureActive()
|
||||
displayTitleMap[item.title] = displayTitle
|
||||
withContext(Main) {
|
||||
notifyItemChanged(i, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayTitleMap[it.title] == null) {
|
||||
displayTitleMap[it.title] = it.getDisplayTitle(replaceRules, useReplace)
|
||||
}.start()
|
||||
async {
|
||||
for (i in startIndex downTo 0) {
|
||||
val item = items[i]
|
||||
if (displayTitleMap[item.title] == null) {
|
||||
ensureActive()
|
||||
val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
|
||||
ensureActive()
|
||||
displayTitleMap[item.title] = displayTitle
|
||||
withContext(Main) {
|
||||
notifyItemChanged(i, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDisplayTitle(chapter: BookChapter): String {
|
||||
return displayTitleMap[chapter.title] ?: chapter.title
|
||||
}
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
|
||||
return ItemChapterListBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
private fun getDisplayTile(chapter: BookChapter): String {
|
||||
var displayTitle = displayTitleMap[chapter.title]
|
||||
if (displayTitle != null) {
|
||||
return displayTitle
|
||||
}
|
||||
displayTitle = runBlocking {
|
||||
chapter.getDisplayTitle(replaceRules, useReplace)
|
||||
}
|
||||
displayTitleMap[chapter.title] = displayTitle
|
||||
return displayTitle
|
||||
}
|
||||
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemChapterListBinding,
|
||||
@@ -120,7 +129,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
} else {
|
||||
tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||
}
|
||||
tvChapterName.text = getDisplayTile(item)
|
||||
tvChapterName.text = getDisplayTitle(item)
|
||||
if (item.isVolume) {
|
||||
//卷名,如第一卷 突出显示
|
||||
tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
|
||||
@@ -138,6 +147,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
}
|
||||
upHasCache(binding, isDur, cached)
|
||||
} else {
|
||||
tvChapterName.text = getDisplayTitle(item)
|
||||
upHasCache(binding, isDur, cached)
|
||||
}
|
||||
}
|
||||
@@ -150,8 +160,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnLongClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
context.longToastOnUi(getDisplayTile(it))
|
||||
getItem(holder.layoutPosition)?.let { item ->
|
||||
context.longToastOnUi(getDisplayTitle(item))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -97,7 +97,15 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
||||
viewModel.bookData.value?.bookUrl?.let { bookUrl ->
|
||||
if (chapter.bookUrl == bookUrl) {
|
||||
adapter.cacheFileNames.add(chapter.getFileName())
|
||||
adapter.notifyItemChanged(chapter.index, true)
|
||||
if (viewModel.searchKey.isNullOrEmpty()) {
|
||||
adapter.notifyItemChanged(chapter.index, true)
|
||||
} else {
|
||||
adapter.getItems().forEachIndexed { index, bookChapter ->
|
||||
if (bookChapter.index == chapter.index) {
|
||||
adapter.notifyItemChanged(index, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,12 +136,13 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
||||
}
|
||||
}
|
||||
mLayoutManager.scrollToPositionWithOffset(scrollPos, 0)
|
||||
adapter.upDisplayTitles(scrollPos)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearDisplayTitle() {
|
||||
adapter.clearDisplayTitle()
|
||||
adapter.upDisplayTitle()
|
||||
adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition())
|
||||
}
|
||||
|
||||
override val scope: CoroutineScope
|
||||
|
||||
@@ -59,10 +59,12 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>() {
|
||||
setOnSearchClickListener { tabLayout.gone() }
|
||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
viewModel.searchKey = query
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
viewModel.searchKey = newText
|
||||
if (tabLayout.selectedTabPosition == 1) {
|
||||
viewModel.startBookmarkSearch(newText)
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ class TocViewModel(application: Application) : BaseViewModel(application) {
|
||||
var bookData = MutableLiveData<Book>()
|
||||
var chapterListCallBack: ChapterListCallBack? = null
|
||||
var bookMarkCallBack: BookmarkCallBack? = null
|
||||
var searchKey: String? = null
|
||||
|
||||
fun initBook(bookUrl: String) {
|
||||
this.bookUrl = bookUrl
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.help.config.AppConfig
|
||||
@@ -25,7 +24,7 @@ import io.legado.app.help.storage.*
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.permission.Permissions
|
||||
import io.legado.app.lib.permission.PermissionsCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.document.HandleFileContract
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
@@ -33,8 +32,9 @@ import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.init.appCtx
|
||||
import kotlin.collections.set
|
||||
|
||||
class BackupConfigFragment : BasePreferenceFragment(),
|
||||
class BackupConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||
@@ -100,34 +100,18 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_config_backup)
|
||||
findPreference<EditTextPreference>(PreferKey.webDavUrl)?.let {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.applyTint(requireContext().accentColor)
|
||||
}
|
||||
}
|
||||
findPreference<EditTextPreference>(PreferKey.webDavAccount)?.let {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.applyTint(requireContext().accentColor)
|
||||
}
|
||||
}
|
||||
findPreference<EditTextPreference>(PreferKey.webDavPassword)?.let {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.applyTint(requireContext().accentColor)
|
||||
editText.inputType =
|
||||
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
||||
}
|
||||
}
|
||||
findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.applyTint(requireContext().accentColor)
|
||||
}
|
||||
}
|
||||
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
|
||||
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
|
||||
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
|
||||
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
|
||||
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
|
||||
findPreference<io.legado.app.ui.widget.prefs.Preference>("web_dav_restore")
|
||||
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
|
||||
?.onLongClick { restoreDir.launch(); true }
|
||||
}
|
||||
|
||||
@@ -226,7 +210,9 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 备份忽略设置
|
||||
*/
|
||||
private fun backupIgnore() {
|
||||
val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
|
||||
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false
|
||||
|
||||
@@ -7,16 +7,16 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.prefs.SwitchPreference
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.ui.widget.prefs.SwitchPreference
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class CoverConfigFragment : BasePreferenceFragment(),
|
||||
class CoverConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestCodeCover = 111
|
||||
|
||||
@@ -6,16 +6,17 @@ import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
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.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.model.CheckSource
|
||||
import io.legado.app.receiver.SharedReceiverActivity
|
||||
@@ -26,7 +27,7 @@ import io.legado.app.utils.*
|
||||
import splitties.init.appCtx
|
||||
|
||||
|
||||
class OtherConfigFragment : BasePreferenceFragment(),
|
||||
class OtherConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||
@@ -131,9 +132,9 @@ class OtherConfigFragment : BasePreferenceFragment(),
|
||||
setProcessTextEnable(it.getBoolean(key, true))
|
||||
}
|
||||
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
|
||||
PreferKey.language -> listView.postDelayed({
|
||||
PreferKey.language -> listView.postDelayed(1000) {
|
||||
appCtx.restart()
|
||||
}, 1000)
|
||||
}
|
||||
PreferKey.userAgent -> listView.post {
|
||||
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.widget.SeekBar
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
@@ -24,16 +23,17 @@ import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.prefs.ColorPreference
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.widget.number.NumberPickerDialog
|
||||
import io.legado.app.ui.widget.prefs.ColorPreference
|
||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
class ThemeConfigFragment : BasePreferenceFragment(),
|
||||
class ThemeConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestCodeBgLight = 121
|
||||
|
||||
@@ -7,15 +7,15 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class WelcomeConfigFragment : BasePreferenceFragment(),
|
||||
class WelcomeConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestWelcomeImage = 221
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
@@ -87,13 +88,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
upVersion()
|
||||
//自动更新书籍
|
||||
if (AppConfig.autoRefreshBook) {
|
||||
binding.viewPagerMain.postDelayed({
|
||||
binding.viewPagerMain.postDelayed(1000) {
|
||||
viewModel.upAllBookToc()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
binding.viewPagerMain.postDelayed({
|
||||
binding.viewPagerMain.postDelayed(3000) {
|
||||
viewModel.postLoad()
|
||||
}, 3000)
|
||||
}
|
||||
launch {
|
||||
val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() }
|
||||
?: return@launch
|
||||
|
||||
@@ -8,13 +8,16 @@ import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseFragment
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.databinding.FragmentMyConfigBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.prefs.NameListPreference
|
||||
import io.legado.app.lib.prefs.PreferenceCategory
|
||||
import io.legado.app.lib.prefs.SwitchPreference
|
||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.ui.about.AboutActivity
|
||||
@@ -26,9 +29,6 @@ import io.legado.app.ui.config.ConfigActivity
|
||||
import io.legado.app.ui.config.ConfigTag
|
||||
import io.legado.app.ui.replace.ReplaceRuleActivity
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.ui.widget.prefs.NameListPreference
|
||||
import io.legado.app.ui.widget.prefs.PreferenceCategory
|
||||
import io.legado.app.ui.widget.prefs.SwitchPreference
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
@@ -40,7 +40,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
||||
setSupportToolbar(binding.titleBar.toolbar)
|
||||
val fragmentTag = "prefFragment"
|
||||
var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
|
||||
if (preferenceFragment == null) preferenceFragment = PreferenceFragment()
|
||||
if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment()
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit()
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
class PreferenceFragment : BasePreferenceFragment(),
|
||||
class MyPreferenceFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.databinding.ActivityRssArtivlesBinding
|
||||
import io.legado.app.databinding.DialogEditTextBinding
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.ui.login.SourceLoginActivity
|
||||
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
@@ -45,9 +46,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
viewModel.titleLiveData.observe(this, {
|
||||
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
viewModel.titleLiveData.observe(this) {
|
||||
binding.titleBar.title = it
|
||||
})
|
||||
}
|
||||
viewModel.initData(intent) {
|
||||
upFragments()
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class RssSourceEditActivity :
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it?.let {
|
||||
viewModel.importSource(it) { source: RssSource ->
|
||||
upRecyclerView(source)
|
||||
upSourceView(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class RssSourceEditActivity :
|
||||
softKeyboardTool.attachToWindow(window)
|
||||
initView()
|
||||
viewModel.initData(intent) {
|
||||
upRecyclerView()
|
||||
upSourceView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,10 +130,11 @@ class RssSourceEditActivity :
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.menu_clear_cookie -> viewModel.clearCookie(getRssSource().sourceUrl)
|
||||
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
||||
R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
|
||||
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
||||
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
|
||||
R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
|
||||
R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
|
||||
R.id.menu_share_qr -> shareWithQr(
|
||||
GSON.toJson(getRssSource()),
|
||||
@@ -150,10 +151,11 @@ class RssSourceEditActivity :
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun upRecyclerView(source: RssSource? = viewModel.rssSource) {
|
||||
private fun upSourceView(source: RssSource? = viewModel.rssSource) {
|
||||
source?.let {
|
||||
binding.cbIsEnable.isChecked = source.enabled
|
||||
binding.cbSingleUrl.isChecked = source.singleUrl
|
||||
binding.cbIsEnableCookie.isChecked = source.enabledCookieJar == true
|
||||
binding.cbEnableJs.isChecked = source.enableJs
|
||||
binding.cbEnableBaseUrl.isChecked = source.loadWithBaseUrl
|
||||
}
|
||||
@@ -191,6 +193,7 @@ class RssSourceEditActivity :
|
||||
val source = viewModel.rssSource
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
source.enableJs = binding.cbEnableJs.isChecked
|
||||
source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked
|
||||
sourceEntities.forEach {
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.help.RuleComplete
|
||||
import io.legado.app.help.http.CookieStore
|
||||
import io.legado.app.utils.getClipText
|
||||
import io.legado.app.utils.msg
|
||||
import io.legado.app.utils.printOnDebug
|
||||
@@ -77,6 +78,12 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookie(url: String) {
|
||||
execute {
|
||||
CookieStore.removeCookie(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
||||
if (autoComplete) {
|
||||
return RuleComplete.autoComplete(rule, preRule, type)
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.welcome
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.postDelayed
|
||||
import io.legado.app.base.BaseActivity
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Theme
|
||||
@@ -26,7 +27,7 @@ open class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
||||
if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) {
|
||||
finish()
|
||||
} else {
|
||||
binding.root.postDelayed({ startMainActivity() }, 600)
|
||||
binding.root.postDelayed(600) { startMainActivity() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.Checkable
|
||||
import androidx.core.view.postDelayed
|
||||
import io.legado.app.R
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.utils.dpToPx
|
||||
@@ -244,7 +245,7 @@ class SmoothCheckBox @JvmOverloads constructor(
|
||||
}
|
||||
// invalidate
|
||||
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {
|
||||
postDelayed({ this.postInvalidate() }, 10)
|
||||
postDelayed(10) { this.postInvalidate() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,10 +299,10 @@ class SmoothCheckBox @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun drawTickDelayed() {
|
||||
postDelayed({
|
||||
postDelayed(mAnimDuration.toLong()) {
|
||||
mTickDrawing = true
|
||||
postInvalidate()
|
||||
}, mAnimDuration.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package io.legado.app.ui.widget.prefs
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import io.legado.app.R
|
||||
|
||||
class EditTextPreference(context: Context, attrs: AttributeSet) :
|
||||
androidx.preference.EditTextPreference(context, attrs) {
|
||||
|
||||
init {
|
||||
// isPersistent = true
|
||||
layoutResource = R.layout.view_preference
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
|
||||
super.onBindViewHolder(holder)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import android.widget.FrameLayout.LayoutParams
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import io.legado.app.R
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import io.legado.app.utils.getCompatColor
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.visible
|
||||
@@ -87,8 +88,6 @@ class BadgeView @JvmOverloads constructor(
|
||||
setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
// set default font
|
||||
setTextColor(Color.WHITE)
|
||||
//setTypeface(Typeface.DEFAULT_BOLD);
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
|
||||
setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
|
||||
@@ -106,6 +105,10 @@ class BadgeView @JvmOverloads constructor(
|
||||
minHeight = dip2Px(16f)
|
||||
}
|
||||
|
||||
override fun setBackgroundColor(color: Int) {
|
||||
setBackground(radius, color)
|
||||
}
|
||||
|
||||
fun setBackground(dipRadius: Float, badgeColor: Int) {
|
||||
val radius = dip2Px(dipRadius).toFloat()
|
||||
val radiusArray =
|
||||
@@ -118,10 +121,13 @@ class BadgeView @JvmOverloads constructor(
|
||||
val bgDrawable = ShapeDrawable(roundRect)
|
||||
bgDrawable.paint.color = badgeColor
|
||||
background = bgDrawable
|
||||
}
|
||||
|
||||
fun setBackground(badgeColor: Int) {
|
||||
setBackground(radius, badgeColor)
|
||||
setTextColor(
|
||||
if (ColorUtils.isColorLight(badgeColor)) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,9 +148,9 @@ class BadgeView @JvmOverloads constructor(
|
||||
|
||||
fun setHighlight(highlight: Boolean) {
|
||||
if (highlight) {
|
||||
setBackground(context.accentColor)
|
||||
setBackgroundColor(context.accentColor)
|
||||
} else {
|
||||
setBackground(context.getCompatColor(R.color.darker_gray))
|
||||
setBackgroundColor(context.getCompatColor(R.color.darker_gray))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import java.util.*
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import kotlin.math.*
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object ColorUtils {
|
||||
|
||||
fun isColorLight(@ColorInt color: Int): Boolean {
|
||||
return ColorUtils.calculateLuminance(color) >= 0.5
|
||||
}
|
||||
|
||||
fun intToString(intColor: Int): String {
|
||||
return String.format("#%06X", 0xFFFFFF and intColor)
|
||||
}
|
||||
|
||||
|
||||
fun stripAlpha(@ColorInt color: Int): Int {
|
||||
return -0x1000000 or color
|
||||
}
|
||||
@@ -39,12 +41,6 @@ object ColorUtils {
|
||||
return shiftColor(color, 1.1f)
|
||||
}
|
||||
|
||||
fun isColorLight(@ColorInt color: Int): Boolean {
|
||||
val darkness =
|
||||
1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
|
||||
return darkness < 0.4
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun invertColor(@ColorInt color: Int): Int {
|
||||
val r = 255 - Color.red(color)
|
||||
@@ -81,83 +77,6 @@ object ColorUtils {
|
||||
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
|
||||
}
|
||||
|
||||
/**
|
||||
* 按条件的到随机颜色
|
||||
*
|
||||
* @param alpha 透明
|
||||
* @param lower 下边界
|
||||
* @param upper 上边界
|
||||
* @return 颜色值
|
||||
*/
|
||||
fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int {
|
||||
return RandomColor(alpha, lower, upper).color
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取随机色
|
||||
*/
|
||||
fun getRandomColor(): Int {
|
||||
return RandomColor(255, 80, 200).color
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 随机颜色
|
||||
*/
|
||||
class RandomColor(alpha: Int, lower: Int, upper: Int) {
|
||||
private var alpha: Int = 0
|
||||
private var lower: Int = 0
|
||||
private var upper: Int = 0
|
||||
|
||||
//随机数是前闭 后开
|
||||
val color: Int
|
||||
get() {
|
||||
val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
|
||||
return Color.argb(getAlpha(), red, green, blue)
|
||||
}
|
||||
|
||||
init {
|
||||
require(upper > lower) { "must be lower < upper" }
|
||||
setAlpha(alpha)
|
||||
setLower(lower)
|
||||
setUpper(upper)
|
||||
}
|
||||
|
||||
private fun getAlpha(): Int {
|
||||
return alpha
|
||||
}
|
||||
|
||||
private fun setAlpha(alpha: Int) {
|
||||
var alpha1 = alpha
|
||||
if (alpha1 > 255) alpha1 = 255
|
||||
if (alpha1 < 0) alpha1 = 0
|
||||
this.alpha = alpha1
|
||||
}
|
||||
|
||||
private fun getLower(): Int {
|
||||
return lower
|
||||
}
|
||||
|
||||
private fun setLower(lower: Int) {
|
||||
var lower1 = lower
|
||||
if (lower1 < 0) lower1 = 0
|
||||
this.lower = lower1
|
||||
}
|
||||
|
||||
private fun getUpper(): Int {
|
||||
return upper
|
||||
}
|
||||
|
||||
private fun setUpper(upper: Int) {
|
||||
var upper1 = upper
|
||||
if (upper1 > 255) upper1 = 255
|
||||
this.upper = upper1
|
||||
}
|
||||
}
|
||||
|
||||
fun argb(R: Int, G: Int, B: Int): Int {
|
||||
return argb(Byte.MAX_VALUE.toInt(), R, G, B)
|
||||
}
|
||||
@@ -177,72 +96,19 @@ object ColorUtils {
|
||||
+ (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))
|
||||
}
|
||||
|
||||
fun rgb2lab(R: Int, G: Int, B: Int): IntArray {
|
||||
val x: Float
|
||||
val y: Float
|
||||
val z: Float
|
||||
val fx: Float
|
||||
val fy: Float
|
||||
val fz: Float
|
||||
val xr: Float
|
||||
val yr: Float
|
||||
val zr: Float
|
||||
val eps = 216f / 24389f
|
||||
val k = 24389f / 27f
|
||||
val xr1 = 0.964221f // reference white D50
|
||||
val yr1 = 1.0f
|
||||
val zr1 = 0.825211f
|
||||
|
||||
// RGB to XYZ
|
||||
var r: Float = R / 255f //R 0..1
|
||||
var g: Float = G / 255f //G 0..1
|
||||
var b: Float = B / 255f //B 0..1
|
||||
|
||||
// assuming sRGB (D65)
|
||||
r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat()
|
||||
g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat()
|
||||
b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat()
|
||||
x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b
|
||||
y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b
|
||||
z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b
|
||||
|
||||
// XYZ to Lab
|
||||
xr = x / xr1
|
||||
yr = y / yr1
|
||||
zr = z / zr1
|
||||
fx = if (xr > eps) xr.toDouble().pow(1 / 3.0)
|
||||
.toFloat() else ((k * xr + 16.0) / 116.0).toFloat()
|
||||
fy = if (yr > eps) yr.toDouble().pow(1 / 3.0)
|
||||
.toFloat() else ((k * yr + 16.0) / 116.0).toFloat()
|
||||
fz = if (zr > eps) zr.toDouble().pow(1 / 3.0)
|
||||
.toFloat() else ((k * zr + 16.0) / 116).toFloat()
|
||||
val ls: Float = 116 * fy - 16
|
||||
val `as`: Float = 500 * (fx - fy)
|
||||
val bs: Float = 200 * (fy - fz)
|
||||
val lab = IntArray(3)
|
||||
lab[0] = (2.55 * ls + .5).toInt()
|
||||
lab[1] = (`as` + .5).toInt()
|
||||
lab[2] = (bs + .5).toInt()
|
||||
return lab
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the difference between two RGB colors by converting them to the L*a*b scale and
|
||||
* comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
|
||||
*/
|
||||
fun getColorDifference(a: Int, b: Int): Double {
|
||||
val r1: Int = Color.red(a)
|
||||
val g1: Int = Color.green(a)
|
||||
val b1: Int = Color.blue(a)
|
||||
val r2: Int = Color.red(b)
|
||||
val g2: Int = Color.green(b)
|
||||
val b2: Int = Color.blue(b)
|
||||
val lab1 = rgb2lab(r1, g1, b1)
|
||||
val lab2 = rgb2lab(r2, g2, b2)
|
||||
val lab1 = DoubleArray(3)
|
||||
val lab2 = DoubleArray(3)
|
||||
ColorUtils.colorToLAB(a, lab1)
|
||||
ColorUtils.colorToLAB(b, lab2)
|
||||
return sqrt(
|
||||
(lab2[0] - lab1[0].toDouble())
|
||||
.pow(2.0) + (lab2[1] - lab1[1].toDouble())
|
||||
.pow(2.0) + (lab2[2] - lab1[2].toDouble())
|
||||
(lab2[0] - lab1[0])
|
||||
.pow(2.0) + (lab2[1] - lab1[1])
|
||||
.pow(2.0) + (lab2[2] - lab1[2])
|
||||
.pow(2.0)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ fun Context.openFileUri(uri: Uri, type: String? = null) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val Context.isWifiConnect: Boolean
|
||||
@SuppressLint("MissingPermission")
|
||||
get() {
|
||||
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
||||
return info?.isConnected == true
|
||||
@@ -338,7 +339,7 @@ val Context.isWifiConnect: Boolean
|
||||
|
||||
val Context.isPad: Boolean
|
||||
get() {
|
||||
return resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
||||
return (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
||||
}
|
||||
|
||||
val Context.channel: String
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.*
|
||||
|
||||
@Suppress("unused")
|
||||
class RandomColor(alpha: Int, lower: Int, upper: Int) {
|
||||
|
||||
constructor() : this(255, 80, 200)
|
||||
|
||||
private var alpha: Int = 0
|
||||
private var lower: Int = 0
|
||||
private var upper: Int = 0
|
||||
|
||||
init {
|
||||
require(upper > lower) { "must be lower < upper" }
|
||||
setAlpha(alpha)
|
||||
setLower(lower)
|
||||
setUpper(upper)
|
||||
}
|
||||
|
||||
//随机数是前闭 后开
|
||||
fun build(): Int {
|
||||
val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||
return Color.argb(getAlpha(), red, green, blue)
|
||||
}
|
||||
|
||||
private fun getAlpha(): Int {
|
||||
return alpha
|
||||
}
|
||||
|
||||
private fun setAlpha(alpha: Int) {
|
||||
var alpha1 = alpha
|
||||
if (alpha1 > 255) alpha1 = 255
|
||||
if (alpha1 < 0) alpha1 = 0
|
||||
this.alpha = alpha1
|
||||
}
|
||||
|
||||
private fun getLower(): Int {
|
||||
return lower
|
||||
}
|
||||
|
||||
private fun setLower(lower: Int) {
|
||||
var lower1 = lower
|
||||
if (lower1 < 0) lower1 = 0
|
||||
this.lower = lower1
|
||||
}
|
||||
|
||||
private fun getUpper(): Int {
|
||||
return upper
|
||||
}
|
||||
|
||||
private fun setUpper(upper: Int) {
|
||||
var upper1 = upper
|
||||
if (upper1 > 255) upper1 = 255
|
||||
this.upper = upper1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import androidx.core.os.postDelayed
|
||||
import io.legado.app.exception.RegexTimeoutException
|
||||
import io.legado.app.help.CrashHandler
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/**
|
||||
* 带有超时检测的正则替换
|
||||
@@ -19,23 +21,23 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon
|
||||
val result = regex.replace(charSequence, replacement)
|
||||
block.resume(result)
|
||||
} catch (e: Exception) {
|
||||
block.cancel(e)
|
||||
block.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
mainHandler.postDelayed({
|
||||
mainHandler.postDelayed(timeout) {
|
||||
if (thread.isAlive) {
|
||||
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
|
||||
val exception = RegexTimeoutException(timeoutMsg)
|
||||
block.cancel(exception)
|
||||
appCtx.longToastOnUi(timeoutMsg)
|
||||
CrashHandler.saveCrashInfo2File(exception)
|
||||
mainHandler.postDelayed({
|
||||
mainHandler.postDelayed(3000) {
|
||||
if (thread.isAlive) {
|
||||
appCtx.restart()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
|
||||
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
||||
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
||||
"/saveBook" -> BookController.saveBook(postData)
|
||||
"/saveBookProgress" -> BookController.saveBookProgress(postData)
|
||||
"/addLocalBook" -> BookController.addLocalBook(session.parameters)
|
||||
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
||||
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
||||
|
||||
Reference in New Issue
Block a user