[新增] 合并了Sigma版本的一些更改(@Luoyacheng),具体合并了什么其实我也不知道,因为我头晕了
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -34,5 +34,6 @@ object EventBus {
|
|||||||
const val EXPORT_BOOK = "exportBook"
|
const val EXPORT_BOOK = "exportBook"
|
||||||
const val UP_MANGA_CONFIG = "upMangaConfig"
|
const val UP_MANGA_CONFIG = "upMangaConfig"
|
||||||
const val PLAY_MODE_CHANGED = "playModeChanged"
|
const val PLAY_MODE_CHANGED = "playModeChanged"
|
||||||
|
const val REFRESH_BOOK_INFO = "refreshBookInfo"
|
||||||
const val UP_TOC = "upToc"
|
const val UP_TOC = "upToc"
|
||||||
}
|
}
|
||||||
@@ -160,6 +160,7 @@ object PreferKey {
|
|||||||
const val showBookshelfFastScroller = "showBookshelfFastScroller"
|
const val showBookshelfFastScroller = "showBookshelfFastScroller"
|
||||||
const val importKeepEnable = "importKeepEnable"
|
const val importKeepEnable = "importKeepEnable"
|
||||||
const val previewImageByClick = "previewImageByClick"
|
const val previewImageByClick = "previewImageByClick"
|
||||||
|
const val clickImgWay = "clickImgWay"
|
||||||
const val keyPageOnLongPress = "keyPageOnLongPress"
|
const val keyPageOnLongPress = "keyPageOnLongPress"
|
||||||
const val volumeKeyPage = "volumeKeyPage"
|
const val volumeKeyPage = "volumeKeyPage"
|
||||||
const val volumeKeyPageOnPlay = "volumeKeyPageOnPlay"
|
const val volumeKeyPageOnPlay = "volumeKeyPageOnPlay"
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import io.legado.app.data.entities.Cookie
|
|||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
import io.legado.app.data.entities.HttpTTS
|
import io.legado.app.data.entities.HttpTTS
|
||||||
import io.legado.app.data.entities.KeyboardAssist
|
import io.legado.app.data.entities.KeyboardAssist
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
import io.legado.app.data.entities.RssArticle
|
import io.legado.app.data.entities.RssArticle
|
||||||
import io.legado.app.data.entities.RssReadRecord
|
import io.legado.app.data.entities.RssReadRecord
|
||||||
@@ -50,6 +49,7 @@ import io.legado.app.data.entities.SearchBook
|
|||||||
import io.legado.app.data.entities.SearchKeyword
|
import io.legado.app.data.entities.SearchKeyword
|
||||||
import io.legado.app.data.entities.Server
|
import io.legado.app.data.entities.Server
|
||||||
import io.legado.app.data.entities.TxtTocRule
|
import io.legado.app.data.entities.TxtTocRule
|
||||||
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||||
import io.legado.app.help.DefaultData
|
import io.legado.app.help.DefaultData
|
||||||
@@ -67,7 +67,7 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 80,
|
version = 81,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
@@ -113,7 +113,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 76, to = 77),
|
AutoMigration(from = 76, to = 77),
|
||||||
AutoMigration(from = 77, to = 78),
|
AutoMigration(from = 77, to = 78),
|
||||||
AutoMigration(from = 78, to = 79),
|
AutoMigration(from = 78, to = 79),
|
||||||
AutoMigration(from = 79, to = 80)
|
AutoMigration(from = 79, to = 80),
|
||||||
|
AutoMigration(from = 80, to = 81)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
package io.legado.app.data.dao
|
package io.legado.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
import io.legado.app.data.entities.RssArticle
|
import io.legado.app.data.entities.RssArticle
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface RssArticleDao {
|
interface RssArticleDao {
|
||||||
|
|
||||||
|
@Query("select * from rssArticles where origin = :origin and link = :link and sort = :sort")
|
||||||
|
fun get(origin: String, link: String, sort: String): RssArticle?
|
||||||
|
|
||||||
@Query("select * from rssArticles where origin = :origin and link = :link")
|
@Query("select * from rssArticles where origin = :origin and link = :link")
|
||||||
fun get(origin: String, link: String): RssArticle?
|
fun getByLink(origin: String, link: String): RssArticle?
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content,
|
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content,
|
||||||
t1.description, t1.image, t1.`group`, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read
|
t1.description, t1.image, t1.`group`, t1.pubDate, t1.variable, t1.type, t1.durPos, ifNull(t2.read, 0) as read
|
||||||
from rssArticles as t1 left join rssReadRecords as t2
|
from rssArticles as t1 left join rssReadRecords as t2
|
||||||
on t1.link = t2.record where origin = :origin and sort = :sort
|
on t1.link = t2.record where t1.origin = :origin and t1.sort = :sort
|
||||||
order by `order` desc"""
|
order by `order` desc"""
|
||||||
)
|
)
|
||||||
fun flowByOriginSort(origin: String, sort: String): Flow<List<RssArticle>>
|
fun flowByOriginSort(origin: String, sort: String): Flow<List<RssArticle>>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.data.entities
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
import cn.hutool.crypto.symmetric.AES
|
import cn.hutool.crypto.symmetric.AES
|
||||||
import com.script.ScriptBindings
|
import com.script.ScriptBindings
|
||||||
import com.script.buildScriptBindings
|
import com.script.buildScriptBindings
|
||||||
@@ -56,7 +57,7 @@ interface BaseSource : JsExtensions {
|
|||||||
*/
|
*/
|
||||||
var jsLib: String?
|
var jsLib: String?
|
||||||
|
|
||||||
fun getTag(): String
|
override fun getTag(): String
|
||||||
|
|
||||||
fun getKey(): String
|
fun getKey(): String
|
||||||
|
|
||||||
@@ -64,6 +65,32 @@ interface BaseSource : JsExtensions {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLoginJs(): String? {
|
||||||
|
val loginJs = loginUrl
|
||||||
|
return when {
|
||||||
|
loginJs == null -> null
|
||||||
|
loginJs.startsWith("@js:") -> loginJs.substring(4)
|
||||||
|
loginJs.startsWith("<js>") -> loginJs.substring(4, loginJs.lastIndexOf("<"))
|
||||||
|
else -> loginJs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun login() {
|
||||||
|
val loginJs = getLoginJs()
|
||||||
|
if (!loginJs.isNullOrBlank()) {
|
||||||
|
@Language("js")
|
||||||
|
val js = """$loginJs
|
||||||
|
if(typeof login=='function'){
|
||||||
|
login.apply(this);
|
||||||
|
} else {
|
||||||
|
throw('Function login not implements!!!')
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
evalJS(js)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loginUi(): List<RowUi>? {
|
fun loginUi(): List<RowUi>? {
|
||||||
val json = loginUi?.let {
|
val json = loginUi?.let {
|
||||||
val loginJS = getLoginJs() ?: ""
|
val loginJS = getLoginJs() ?: ""
|
||||||
@@ -88,34 +115,6 @@ interface BaseSource : JsExtensions {
|
|||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLoginJs(): String? {
|
|
||||||
val loginJs = loginUrl
|
|
||||||
return when {
|
|
||||||
loginJs == null -> null
|
|
||||||
loginJs.startsWith("@js:") -> loginJs.substring(4)
|
|
||||||
loginJs.startsWith("<js>") -> loginJs.substring(4, loginJs.lastIndexOf("<"))
|
|
||||||
else -> loginJs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 调用login函数 实现登录请求
|
|
||||||
*/
|
|
||||||
fun login() {
|
|
||||||
val loginJs = getLoginJs()
|
|
||||||
if (!loginJs.isNullOrBlank()) {
|
|
||||||
@Language("js")
|
|
||||||
val js = """$loginJs
|
|
||||||
if(typeof login=='function'){
|
|
||||||
login.apply(this);
|
|
||||||
} else {
|
|
||||||
throw('Function login not implements!!!')
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
evalJS(js)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析header规则
|
* 解析header规则
|
||||||
*/
|
*/
|
||||||
@@ -194,9 +193,40 @@ interface BaseSource : JsExtensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLoginInfoMap(): Map<String, String> {
|
private fun configureScriptBindings(): ScriptBindings.() -> Unit = {
|
||||||
val json = getLoginInfo() ?: return emptyMap()
|
put("result", mutableMapOf<String, String>())
|
||||||
return GSON.fromJsonObject<Map<String, String>>(json).getOrNull() ?: emptyMap()
|
put("book", null)
|
||||||
|
put("chapter", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLoginInfoMap(): MutableMap<String, String> {
|
||||||
|
val json = getLoginInfo() ?: if (loginUi.isNullOrBlank()) {
|
||||||
|
return mutableMapOf()
|
||||||
|
} else {
|
||||||
|
val loginUiJson = loginUi?.let {
|
||||||
|
when {
|
||||||
|
it.startsWith("@js:") -> evalJS(
|
||||||
|
"${getLoginJs() ?: ""}\n${it.substring(4)}",
|
||||||
|
configureScriptBindings()
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
it.startsWith("<js>") -> evalJS(
|
||||||
|
"${getLoginJs() ?: ""}\n${it.substring(4, it.lastIndexOf("<"))}",
|
||||||
|
configureScriptBindings()
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val longinInfo = GSON.fromJsonArray<RowUi>(loginUiJson).getOrNull()
|
||||||
|
?.filter { it.type != "button" }
|
||||||
|
?.associate { it.name to (it.default ?: "") }
|
||||||
|
?.takeIf { it.isNotEmpty() }?.also {
|
||||||
|
putLoginInfo(GSON.toJson(it))
|
||||||
|
}
|
||||||
|
return longinInfo?.toMutableMap() ?: mutableMapOf()
|
||||||
|
}
|
||||||
|
return GSON.fromJsonObject<MutableMap<String, String>>(json).getOrNull() ?: mutableMapOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import kotlinx.parcelize.IgnoredOnParcel
|
|||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "rssArticles",
|
tableName = "rssArticles",
|
||||||
primaryKeys = ["origin", "link"]
|
primaryKeys = ["origin", "link", "sort"]
|
||||||
)
|
)
|
||||||
data class RssArticle(
|
data class RssArticle(
|
||||||
override var origin: String = "",
|
override var origin: String = "",
|
||||||
@@ -24,14 +24,20 @@ data class RssArticle(
|
|||||||
@ColumnInfo(defaultValue = "默认分组")
|
@ColumnInfo(defaultValue = "默认分组")
|
||||||
var group: String = "默认分组",
|
var group: String = "默认分组",
|
||||||
var read: Boolean = false,
|
var read: Boolean = false,
|
||||||
override var variable: String? = null
|
override var variable: String? = null,
|
||||||
|
/**类型 0网页,1图片,2视频**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var type: Int = 0,
|
||||||
|
/**阅读进度**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var durPos: Int = 0
|
||||||
) : BaseRssArticle {
|
) : BaseRssArticle {
|
||||||
|
|
||||||
override fun hashCode() = link.hashCode()
|
override fun hashCode() = link.hashCode()
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
other ?: return false
|
other ?: return false
|
||||||
return if (other is RssArticle) origin == other.origin && link == other.link else false
|
return if (other is RssArticle) origin == other.origin && link == other.link && sort == other.sort else false
|
||||||
}
|
}
|
||||||
|
|
||||||
@delegate:Transient
|
@delegate:Transient
|
||||||
@@ -52,6 +58,20 @@ data class RssArticle(
|
|||||||
content = content,
|
content = content,
|
||||||
image = image,
|
image = image,
|
||||||
group = group,
|
group = group,
|
||||||
variable = variable
|
variable = variable,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toRecord() = RssReadRecord(
|
||||||
|
origin = origin,
|
||||||
|
sort = sort,
|
||||||
|
title = title,
|
||||||
|
readTime = System.currentTimeMillis(),
|
||||||
|
record = link,
|
||||||
|
image = image,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos,
|
||||||
|
pubDate = pubDate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,53 @@
|
|||||||
package io.legado.app.data.entities
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@Entity(tableName = "rssReadRecords")
|
@Entity(tableName = "rssReadRecords", indices = [Index(value = ["origin"], unique = false)])
|
||||||
data class RssReadRecord(
|
data class RssReadRecord(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val record: String,
|
val record: String,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val readTime: Long? = null,
|
val readTime: Long? = null,
|
||||||
val read: Boolean = true
|
val read: Boolean = true,
|
||||||
|
@ColumnInfo(defaultValue = "")
|
||||||
|
val origin: String = "",
|
||||||
|
@ColumnInfo(defaultValue = "")
|
||||||
|
var sort: String = "",
|
||||||
|
var image: String? = null,
|
||||||
|
/**类型 0网页,1图片,2视频**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var type: Int = 0,
|
||||||
|
/**阅读进度**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var durPos: Int = 0,
|
||||||
|
var pubDate: String? = null
|
||||||
|
) {
|
||||||
|
fun toRssArticle(): RssArticle {
|
||||||
|
return RssArticle(
|
||||||
|
title = title ?: "",
|
||||||
|
origin = origin,
|
||||||
|
link = record,
|
||||||
|
sort = sort,
|
||||||
|
image = image,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos,
|
||||||
|
pubDate = pubDate
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toStar(): RssStar {
|
||||||
|
return RssStar(
|
||||||
|
title = title ?: "",
|
||||||
|
origin = origin,
|
||||||
|
link = record,
|
||||||
|
sort = sort,
|
||||||
|
image = image,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos,
|
||||||
|
pubDate = pubDate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,13 @@ data class RssStar(
|
|||||||
var image: String? = null,
|
var image: String? = null,
|
||||||
@ColumnInfo(defaultValue = "默认分组")
|
@ColumnInfo(defaultValue = "默认分组")
|
||||||
var group: String = "默认分组",
|
var group: String = "默认分组",
|
||||||
override var variable: String? = null
|
override var variable: String? = null,
|
||||||
|
/**类型 0网页,1图片,2视频**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var type: Int = 0,
|
||||||
|
/**阅读进度**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var durPos: Int = 0
|
||||||
) : BaseRssArticle {
|
) : BaseRssArticle {
|
||||||
|
|
||||||
@delegate:Transient
|
@delegate:Transient
|
||||||
@@ -44,6 +50,20 @@ data class RssStar(
|
|||||||
content = content,
|
content = content,
|
||||||
image = image,
|
image = image,
|
||||||
group = group,
|
group = group,
|
||||||
variable = variable
|
variable = variable,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toRecord() = RssReadRecord(
|
||||||
|
origin = origin,
|
||||||
|
sort = sort,
|
||||||
|
title = title,
|
||||||
|
readTime = System.currentTimeMillis(),
|
||||||
|
record = link,
|
||||||
|
image = image,
|
||||||
|
type = type,
|
||||||
|
durPos = durPos,
|
||||||
|
pubDate = pubDate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ data class FlexChildStyle(
|
|||||||
val layout_alignSelf: String = "auto",
|
val layout_alignSelf: String = "auto",
|
||||||
val layout_flexBasisPercent: Float = -1F,
|
val layout_flexBasisPercent: Float = -1F,
|
||||||
val layout_wrapBefore: Boolean = false,
|
val layout_wrapBefore: Boolean = false,
|
||||||
|
/** 自定义的内部水平对齐属性 **/
|
||||||
|
val layout_justifySelf: String = "auto"
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun alignSelf(): Int {
|
fun alignSelf(): Int {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package io.legado.app.data.entities.rule
|
package io.legado.app.data.entities.rule
|
||||||
|
|
||||||
data class RowUi(
|
data class RowUi(
|
||||||
var name: String = "",
|
val name: String = "",
|
||||||
var type: String = "text",
|
val type: String = "text",
|
||||||
var action: String? = null,
|
val action: String? = null,
|
||||||
var style: FlexChildStyle? = null
|
val chars: Array<String?>? = null,
|
||||||
|
val default: String? = null,
|
||||||
|
var viewName: String? = null,
|
||||||
|
val style: FlexChildStyle? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Suppress("ConstPropertyName")
|
@Suppress("ConstPropertyName")
|
||||||
@@ -13,6 +16,8 @@ data class RowUi(
|
|||||||
const val text = "text"
|
const val text = "text"
|
||||||
const val password = "password"
|
const val password = "password"
|
||||||
const val button = "button"
|
const val button = "button"
|
||||||
|
const val toggle = "toggle"
|
||||||
|
const val select = "select"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,4 +25,21 @@ data class RowUi(
|
|||||||
return style ?: FlexChildStyle.defaultStyle
|
return style ?: FlexChildStyle.defaultStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is RowUi) {
|
||||||
|
return other.name == name
|
||||||
|
&& other.type == type
|
||||||
|
&& other.action == action
|
||||||
|
&& other.default == default
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = name.hashCode() + type.hashCode()
|
||||||
|
result = 31 * result + (action?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (default?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
|||||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
|
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
||||||
import io.legado.app.ui.main.my.MyViewModel
|
import io.legado.app.ui.main.my.MyViewModel
|
||||||
import io.legado.app.ui.replace.ReplaceEditRoute
|
import io.legado.app.ui.replace.ReplaceEditRoute
|
||||||
import io.legado.app.ui.replace.ReplaceRuleViewModel
|
import io.legado.app.ui.replace.ReplaceRuleViewModel
|
||||||
@@ -43,6 +44,7 @@ val appModule = module {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel { ReplaceRuleViewModel(androidApplication()) }
|
viewModel { ReplaceRuleViewModel(androidApplication()) }
|
||||||
|
viewModel { DictRuleViewModel(androidApplication()) }
|
||||||
viewModel { ExploreShowViewModel(get()) }
|
viewModel { ExploreShowViewModel(get()) }
|
||||||
viewModel { SearchContentViewModel(get(), get()) }
|
viewModel { SearchContentViewModel(get(), get()) }
|
||||||
viewModel { MyViewModel(get()) }
|
viewModel { MyViewModel(get()) }
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ package io.legado.app.exception
|
|||||||
/**
|
/**
|
||||||
* 并发限制
|
* 并发限制
|
||||||
*/
|
*/
|
||||||
class ConcurrentException(msg: String, val waitTime: Int) : NoStackTraceException(msg)
|
class ConcurrentException(msg: String, val waitTime: Long) : NoStackTraceException(msg)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.help
|
package io.legado.app.help
|
||||||
|
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
@@ -60,8 +61,9 @@ object CacheManager {
|
|||||||
when (value) {
|
when (value) {
|
||||||
is ByteArray -> ACache.get().put(key, value, saveTime)
|
is ByteArray -> ACache.get().put(key, value, saveTime)
|
||||||
else -> {
|
else -> {
|
||||||
val cache = Cache(key, value.toString(), deadline)
|
val valueStr = value.toString()
|
||||||
putMemory(key, value)
|
putMemory(key, valueStr)
|
||||||
|
val cache = Cache(key, valueStr, deadline)
|
||||||
appDb.cacheDao.insert(cache)
|
appDb.cacheDao.insert(cache)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,26 +88,50 @@ object CacheManager {
|
|||||||
}
|
}
|
||||||
val cache = appDb.cacheDao.get(key)
|
val cache = appDb.cacheDao.get(key)
|
||||||
if (cache != null && (cache.deadline == 0L || cache.deadline > System.currentTimeMillis())) {
|
if (cache != null && (cache.deadline == 0L || cache.deadline > System.currentTimeMillis())) {
|
||||||
putMemory(key, cache.value ?: "")
|
return cache.value?.also {
|
||||||
|
putMemory(key, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(key: String, onlyDisk: Boolean): String? {
|
||||||
|
if (!onlyDisk) {
|
||||||
|
return get(key)
|
||||||
|
}
|
||||||
|
val cache = appDb.cacheDao.get(key)
|
||||||
|
if (cache != null && (cache.deadline == 0L || cache.deadline > System.currentTimeMillis())) {
|
||||||
return cache.value
|
return cache.value
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getInt(key: String): Int? {
|
fun getInt(key: String): Int? {
|
||||||
return get(key)?.toIntOrNull()
|
getFromMemory(key)?.let {
|
||||||
|
if (it is Int) return it
|
||||||
|
}
|
||||||
|
return get(key, true)?.toIntOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLong(key: String): Long? {
|
fun getLong(key: String): Long? {
|
||||||
return get(key)?.toLongOrNull()
|
getFromMemory(key)?.let {
|
||||||
|
if (it is Long) return it
|
||||||
|
}
|
||||||
|
return get(key, true)?.toLongOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDouble(key: String): Double? {
|
fun getDouble(key: String): Double? {
|
||||||
return get(key)?.toDoubleOrNull()
|
getFromMemory(key)?.let {
|
||||||
|
if (it is Double) return it
|
||||||
|
}
|
||||||
|
return get(key, true)?.toDoubleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFloat(key: String): Float? {
|
fun getFloat(key: String): Float? {
|
||||||
return get(key)?.toFloatOrNull()
|
getFromMemory(key)?.let {
|
||||||
|
if (it is Float) return it
|
||||||
|
}
|
||||||
|
return get(key, true)?.toFloatOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getByteArray(key: String): ByteArray? {
|
fun getByteArray(key: String): ByteArray? {
|
||||||
@@ -126,3 +152,50 @@ object CacheManager {
|
|||||||
ACache.get().remove(key)
|
ACache.get().remove(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object WebCacheManager {
|
||||||
|
@JavascriptInterface
|
||||||
|
fun put(key: String, value: String, saveTime: Int = 0) {
|
||||||
|
CacheManager.put(key, value, saveTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun putMemory(key: String, value: String) {
|
||||||
|
memoryLruCache.put(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getFromMemory(key: String): String? {
|
||||||
|
return memoryLruCache[key] as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun deleteMemory(key: String) {
|
||||||
|
memoryLruCache.remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun get(key: String): String? {
|
||||||
|
return CacheManager.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun get(key: String, onlyDisk: Boolean): String? {
|
||||||
|
return CacheManager.get(key, onlyDisk)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun putFile(key: String, value: String, saveTime: Int = 0) {
|
||||||
|
CacheManager.putFile(key, value, saveTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getFile(key: String): String? {
|
||||||
|
return CacheManager.getFile(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun delete(key: String) {
|
||||||
|
CacheManager.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,71 +4,95 @@ import io.legado.app.data.entities.BaseSource
|
|||||||
import io.legado.app.exception.ConcurrentException
|
import io.legado.app.exception.ConcurrentException
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl.ConcurrentRecord
|
import io.legado.app.model.analyzeRule.AnalyzeUrl.ConcurrentRecord
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class ConcurrentRateLimiter(val source: BaseSource?) {
|
class ConcurrentRateLimiter(source: BaseSource?) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val concurrentRecordMap = hashMapOf<String, ConcurrentRecord>()
|
val concurrentRecordMap = ConcurrentHashMap<String, ConcurrentRecord>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新并发率
|
||||||
|
*/
|
||||||
|
fun updateConcurrentRate(key: String, concurrentRate: String) {
|
||||||
|
concurrentRecordMap.compute(key) { _, record ->
|
||||||
|
try {
|
||||||
|
val rateIndex = concurrentRate.indexOf("/")
|
||||||
|
when {
|
||||||
|
rateIndex > 0 -> {
|
||||||
|
val accessLimit = concurrentRate.take(rateIndex).toInt()
|
||||||
|
val interval = concurrentRate.substring(rateIndex + 1).toInt()
|
||||||
|
if (accessLimit <= 0 || interval <= 0) throw NumberFormatException()
|
||||||
|
ConcurrentRecord(
|
||||||
|
record?.time ?: System.currentTimeMillis(),
|
||||||
|
accessLimit,
|
||||||
|
interval,
|
||||||
|
record?.frequency ?: 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
concurrentRate.toInt() > 0 -> {
|
||||||
|
ConcurrentRecord(
|
||||||
|
record?.time ?: System.currentTimeMillis(),
|
||||||
|
1,
|
||||||
|
concurrentRate.toInt(),
|
||||||
|
record?.frequency ?: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> record
|
||||||
|
}
|
||||||
|
} catch (_: NumberFormatException) {
|
||||||
|
record
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val concurrentRate = source?.concurrentRate
|
||||||
|
private val key = source?.getKey()
|
||||||
/**
|
/**
|
||||||
* 开始访问,并发判断
|
* 开始访问,并发判断
|
||||||
*/
|
*/
|
||||||
@Throws(ConcurrentException::class)
|
@Throws(ConcurrentException::class)
|
||||||
private fun fetchStart(): ConcurrentRecord? {
|
private fun fetchStart(): ConcurrentRecord? {
|
||||||
source ?: return null
|
|
||||||
val concurrentRate = source.concurrentRate
|
|
||||||
if (concurrentRate.isNullOrEmpty() || concurrentRate == "0") {
|
if (concurrentRate.isNullOrEmpty() || concurrentRate == "0") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
val key = key ?: return null
|
||||||
|
var isNewRecord = false
|
||||||
|
val fetchRecord = concurrentRecordMap.computeIfAbsent(key) {
|
||||||
|
isNewRecord = true
|
||||||
val rateIndex = concurrentRate.indexOf("/")
|
val rateIndex = concurrentRate.indexOf("/")
|
||||||
var fetchRecord = concurrentRecordMap[source.getKey()]
|
if (rateIndex > 0) {
|
||||||
if (fetchRecord == null) {
|
val accessLimit = concurrentRate.take(rateIndex).toIntOrNull() ?: 1
|
||||||
synchronized(concurrentRecordMap) {
|
val interval = concurrentRate.substring(rateIndex + 1).toIntOrNull() ?: 0
|
||||||
fetchRecord = concurrentRecordMap[source.getKey()]
|
ConcurrentRecord(System.currentTimeMillis(), accessLimit, interval, 1)
|
||||||
if (fetchRecord == null) {
|
|
||||||
fetchRecord = ConcurrentRecord(rateIndex > 0, System.currentTimeMillis(), 1)
|
|
||||||
concurrentRecordMap[source.getKey()] = fetchRecord
|
|
||||||
return fetchRecord
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val waitTime: Int = synchronized(fetchRecord!!) {
|
|
||||||
try {
|
|
||||||
if (!fetchRecord.isConcurrent) {
|
|
||||||
//并发控制非 次数/毫秒
|
|
||||||
if (fetchRecord.frequency > 0) {
|
|
||||||
//已经有访问线程,直接等待
|
|
||||||
return@synchronized concurrentRate.toInt()
|
|
||||||
}
|
|
||||||
//没有线程访问,判断还剩多少时间可以访问
|
|
||||||
val nextTime = fetchRecord.time + concurrentRate.toInt()
|
|
||||||
if (System.currentTimeMillis() >= nextTime) {
|
|
||||||
fetchRecord.time = System.currentTimeMillis()
|
|
||||||
fetchRecord.frequency = 1
|
|
||||||
return@synchronized 0
|
|
||||||
}
|
|
||||||
return@synchronized (nextTime - System.currentTimeMillis()).toInt()
|
|
||||||
} else {
|
} else {
|
||||||
//并发控制为 次数/毫秒
|
ConcurrentRecord(
|
||||||
val sj = concurrentRate.substring(rateIndex + 1)
|
System.currentTimeMillis(),
|
||||||
val nextTime = fetchRecord.time + sj.toInt()
|
1,
|
||||||
if (System.currentTimeMillis() >= nextTime) {
|
concurrentRate.toIntOrNull() ?: 0,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNewRecord) return fetchRecord
|
||||||
|
val waitTime: Long = synchronized(fetchRecord) {
|
||||||
|
//并发控制为 次数/毫秒 , 非并发实际为1/毫秒
|
||||||
|
val nextTime = fetchRecord.time + fetchRecord.interval.toLong()
|
||||||
|
val nowTime = System.currentTimeMillis()
|
||||||
|
if (nowTime >= nextTime) {
|
||||||
//已经过了限制时间,重置开始时间
|
//已经过了限制时间,重置开始时间
|
||||||
fetchRecord.time = System.currentTimeMillis()
|
fetchRecord.time = nowTime
|
||||||
fetchRecord.frequency = 1
|
fetchRecord.frequency = 1
|
||||||
return@synchronized 0
|
return@synchronized 0
|
||||||
}
|
}
|
||||||
val cs = concurrentRate.substring(0, rateIndex)
|
if (fetchRecord.frequency < fetchRecord.accessLimit) {
|
||||||
if (fetchRecord.frequency > cs.toInt()) {
|
fetchRecord.frequency++
|
||||||
return@synchronized (nextTime - System.currentTimeMillis()).toInt()
|
return@synchronized 0
|
||||||
} else {
|
} else {
|
||||||
fetchRecord.frequency += 1
|
return@synchronized nextTime - nowTime
|
||||||
return@synchronized 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_: Exception) {
|
|
||||||
return@synchronized 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (waitTime > 0) {
|
if (waitTime > 0) {
|
||||||
@@ -80,17 +104,6 @@ class ConcurrentRateLimiter(val source: BaseSource?) {
|
|||||||
return fetchRecord
|
return fetchRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 访问结束
|
|
||||||
*/
|
|
||||||
fun fetchEnd(concurrentRecord: ConcurrentRecord?) {
|
|
||||||
if (concurrentRecord != null && !concurrentRecord.isConcurrent) {
|
|
||||||
synchronized(concurrentRecord) {
|
|
||||||
concurrentRecord.frequency -= 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取并发记录,若处于并发限制状态下则会等待
|
* 获取并发记录,若处于并发限制状态下则会等待
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +112,7 @@ class ConcurrentRateLimiter(val source: BaseSource?) {
|
|||||||
try {
|
try {
|
||||||
return fetchStart()
|
return fetchStart()
|
||||||
} catch (e: ConcurrentException) {
|
} catch (e: ConcurrentException) {
|
||||||
delay(e.waitTime.toLong())
|
delay(e.waitTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,27 +122,19 @@ class ConcurrentRateLimiter(val source: BaseSource?) {
|
|||||||
try {
|
try {
|
||||||
return fetchStart()
|
return fetchStart()
|
||||||
} catch (e: ConcurrentException) {
|
} catch (e: ConcurrentException) {
|
||||||
Thread.sleep(e.waitTime.toLong())
|
Thread.sleep(e.waitTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend inline fun <T> withLimit(block: () -> T): T {
|
suspend inline fun <T> withLimit(block: () -> T): T {
|
||||||
val concurrentRecord = getConcurrentRecord()
|
getConcurrentRecord()
|
||||||
try {
|
|
||||||
return block()
|
return block()
|
||||||
} finally {
|
|
||||||
fetchEnd(concurrentRecord)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> withLimitBlocking(block: () -> T): T {
|
inline fun <T> withLimitBlocking(block: () -> T): T {
|
||||||
val concurrentRecord = getConcurrentRecordBlocking()
|
getConcurrentRecordBlocking()
|
||||||
try {
|
|
||||||
return block()
|
return block()
|
||||||
} finally {
|
|
||||||
fetchEnd(concurrentRecord)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package io.legado.app.help
|
package io.legado.app.help
|
||||||
|
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import androidx.core.net.toUri
|
||||||
import cn.hutool.core.codec.Base64
|
import cn.hutool.core.codec.Base64
|
||||||
import cn.hutool.core.util.HexUtil
|
import cn.hutool.core.util.HexUtil
|
||||||
import com.script.rhino.rhinoContext
|
import com.script.rhino.rhinoContext
|
||||||
@@ -13,6 +15,7 @@ import io.legado.app.constant.AppPattern
|
|||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.http.BackstageWebView
|
import io.legado.app.help.http.BackstageWebView
|
||||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||||
import io.legado.app.help.http.CookieStore
|
import io.legado.app.help.http.CookieStore
|
||||||
@@ -23,6 +26,7 @@ import io.legado.app.help.source.getSourceType
|
|||||||
import io.legado.app.model.Debug
|
import io.legado.app.model.Debug
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.model.analyzeRule.QueryTTF
|
import io.legado.app.model.analyzeRule.QueryTTF
|
||||||
|
import io.legado.app.ui.association.OnLineImportActivity
|
||||||
import io.legado.app.ui.association.OpenUrlConfirmActivity
|
import io.legado.app.ui.association.OpenUrlConfirmActivity
|
||||||
import io.legado.app.utils.ArchiveUtils
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import io.legado.app.utils.ChineseUtils
|
import io.legado.app.utils.ChineseUtils
|
||||||
@@ -72,6 +76,7 @@ import java.util.zip.ZipInputStream
|
|||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* js扩展类, 在js中通过java变量调用
|
* js扩展类, 在js中通过java变量调用
|
||||||
* 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md
|
* 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md
|
||||||
@@ -83,24 +88,34 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||||||
interface JsExtensions : JsEncodeUtils {
|
interface JsExtensions : JsEncodeUtils {
|
||||||
|
|
||||||
fun getSource(): BaseSource?
|
fun getSource(): BaseSource?
|
||||||
|
fun getTag(): String?
|
||||||
|
|
||||||
private val context: CoroutineContext
|
private val context: CoroutineContext
|
||||||
get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext
|
get() = rhinoContextOrNull?.coroutineContext ?: EmptyCoroutineContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问网络,返回String
|
* 访问网络,返回String
|
||||||
*/
|
*/
|
||||||
fun ajax(url: Any): String? {
|
fun ajax(url: Any): String? {
|
||||||
|
return ajax(url, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ajax(url: Any, callTimeout: Long?): String? {
|
||||||
val urlStr = if (url is List<*>) {
|
val urlStr = if (url is List<*>) {
|
||||||
url.firstOrNull().toString()
|
url.firstOrNull().toString()
|
||||||
} else {
|
} else {
|
||||||
url.toString()
|
url.toString()
|
||||||
}
|
}
|
||||||
val analyzeUrl = AnalyzeUrl(urlStr, source = getSource(), coroutineContext = context)
|
val analyzeUrl = AnalyzeUrl(
|
||||||
|
urlStr,
|
||||||
|
source = getSource(),
|
||||||
|
callTimeout = callTimeout,
|
||||||
|
coroutineContext = context
|
||||||
|
)
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
analyzeUrl.getStrResponse().body
|
analyzeUrl.getStrResponse().body
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it)
|
AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
it.stackTraceStr
|
it.stackTraceStr
|
||||||
@@ -111,6 +126,10 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* 并发访问网络
|
* 并发访问网络
|
||||||
*/
|
*/
|
||||||
fun ajaxAll(urlList: Array<String>): Array<StrResponse> {
|
fun ajaxAll(urlList: Array<String>): Array<StrResponse> {
|
||||||
|
return ajaxAll(urlList, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ajaxAll(urlList: Array<String>, skipRateLimit: Boolean): Array<StrResponse> {
|
||||||
return runBlocking(context) {
|
return runBlocking(context) {
|
||||||
urlList.asFlow().mapAsync(AppConfig.threadCount) { url ->
|
urlList.asFlow().mapAsync(AppConfig.threadCount) { url ->
|
||||||
val analyzeUrl = AnalyzeUrl(
|
val analyzeUrl = AnalyzeUrl(
|
||||||
@@ -118,11 +137,37 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
source = getSource(),
|
source = getSource(),
|
||||||
coroutineContext = coroutineContext
|
coroutineContext = coroutineContext
|
||||||
)
|
)
|
||||||
analyzeUrl.getStrResponseAwait()
|
analyzeUrl.getStrResponseAwait(skipRateLimit = skipRateLimit)
|
||||||
}.flowOn(IO).toList().toTypedArray()
|
}.flowOn(IO).toList().toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 并发测试网络
|
||||||
|
*/
|
||||||
|
fun ajaxTestAll(urlList: Array<String>, timeout: Int): Array<StrResponse> {
|
||||||
|
return ajaxTestAll(urlList, timeout, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ajaxTestAll(
|
||||||
|
urlList: Array<String>,
|
||||||
|
timeout: Int,
|
||||||
|
skipRateLimit: Boolean
|
||||||
|
): Array<StrResponse> {
|
||||||
|
return runBlocking(context) {
|
||||||
|
urlList.asFlow().mapAsync(AppConfig.threadCount) { url ->
|
||||||
|
val analyzeUrl = AnalyzeUrl(
|
||||||
|
url,
|
||||||
|
source = getSource(),
|
||||||
|
coroutineContext = coroutineContext,
|
||||||
|
callTimeout = timeout.toLong()
|
||||||
|
)
|
||||||
|
analyzeUrl.getStrResponseAwait(isTest = true, skipRateLimit = skipRateLimit)
|
||||||
|
}.flowOn(IO).toList().toTypedArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 访问网络,返回Response<String>
|
* 访问网络,返回Response<String>
|
||||||
*/
|
*/
|
||||||
@@ -135,7 +180,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
analyzeUrl.getStrResponse()
|
analyzeUrl.getStrResponse()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
AppLog.put("connect(${urlStr}) error\n${it.localizedMessage}", it)
|
AppLog.put("connect(${urlStr}) error\n${it.localizedMessage}", it)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
StrResponse(analyzeUrl.url, it.stackTraceStr)
|
StrResponse(analyzeUrl.url, it.stackTraceStr)
|
||||||
@@ -143,31 +188,41 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun connect(urlStr: String, header: String?): StrResponse {
|
fun connect(urlStr: String, header: String?): StrResponse {
|
||||||
|
return connect(urlStr, header, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun connect(urlStr: String, header: String?, callTimeout: Long?): StrResponse {
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(header).getOrNull()
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(header).getOrNull()
|
||||||
val analyzeUrl = AnalyzeUrl(
|
val analyzeUrl = AnalyzeUrl(
|
||||||
urlStr,
|
urlStr,
|
||||||
headerMapF = headerMap,
|
headerMapF = headerMap,
|
||||||
source = getSource(),
|
source = getSource(),
|
||||||
|
callTimeout = callTimeout,
|
||||||
coroutineContext = context
|
coroutineContext = context
|
||||||
)
|
)
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
analyzeUrl.getStrResponse()
|
analyzeUrl.getStrResponse()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
AppLog.put("ajax($urlStr,$header) error\n${it.localizedMessage}", it)
|
AppLog.put("connect($urlStr,$header) error\n${it.localizedMessage}", it)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
StrResponse(analyzeUrl.url, it.stackTraceStr)
|
StrResponse(analyzeUrl.url, it.stackTraceStr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun webView(html: String?, url: String?, js: String?): String? {
|
||||||
|
return webView(html, url, js, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用webView访问网络
|
* 使用webView访问网络
|
||||||
* @param html 直接用webView载入的html, 如果html为空直接访问url
|
* @param html 直接用webView载入的html, 如果html为空直接访问url
|
||||||
* @param url html内如果有相对路径的资源不传入url访问不了
|
* @param url html内如果有相对路径的资源不传入url访问不了
|
||||||
* @param js 用来取返回值的js语句, 没有就返回整个源代码
|
* @param js 用来取返回值的js语句, 没有就返回整个源代码
|
||||||
|
* @param cacheFirst 优先使用缓存,为true能提高访问速度
|
||||||
* @return 返回js获取的内容
|
* @return 返回js获取的内容
|
||||||
*/
|
*/
|
||||||
fun webView(html: String?, url: String?, js: String?): String? {
|
fun webView(html: String?, url: String?, js: String?, cacheFirst: Boolean): String? {
|
||||||
if (isMainThread) {
|
if (isMainThread) {
|
||||||
error("webView must be called on a background thread")
|
error("webView must be called on a background thread")
|
||||||
}
|
}
|
||||||
@@ -182,10 +237,20 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? {
|
||||||
|
return webViewGetSource(html, url, js, sourceRegex, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用webView获取资源url
|
* 使用webView获取资源url
|
||||||
*/
|
*/
|
||||||
fun webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? {
|
fun webViewGetSource(
|
||||||
|
html: String?,
|
||||||
|
url: String?,
|
||||||
|
js: String?,
|
||||||
|
sourceRegex: String,
|
||||||
|
cacheFirst: Boolean
|
||||||
|
): String? {
|
||||||
if (isMainThread) {
|
if (isMainThread) {
|
||||||
error("webViewGetSource must be called on a background thread")
|
error("webViewGetSource must be called on a background thread")
|
||||||
}
|
}
|
||||||
@@ -201,6 +266,15 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun webViewGetOverrideUrl(
|
||||||
|
html: String?,
|
||||||
|
url: String?,
|
||||||
|
js: String?,
|
||||||
|
overrideUrlRegex: String
|
||||||
|
): String? {
|
||||||
|
return webViewGetOverrideUrl(html, url, js, overrideUrlRegex, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用webView获取跳转url
|
* 使用webView获取跳转url
|
||||||
*/
|
*/
|
||||||
@@ -208,7 +282,8 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
html: String?,
|
html: String?,
|
||||||
url: String?,
|
url: String?,
|
||||||
js: String?,
|
js: String?,
|
||||||
overrideUrlRegex: String
|
overrideUrlRegex: String,
|
||||||
|
cacheFirst: Boolean
|
||||||
): String? {
|
): String? {
|
||||||
if (isMainThread) {
|
if (isMainThread) {
|
||||||
error("webViewGetOverrideUrl must be called on a background thread")
|
error("webViewGetOverrideUrl must be called on a background thread")
|
||||||
@@ -231,23 +306,37 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param title 浏览器页面的标题
|
* @param title 浏览器页面的标题
|
||||||
*/
|
*/
|
||||||
fun startBrowser(url: String, title: String) {
|
fun startBrowser(url: String, title: String) {
|
||||||
|
return startBrowser(url, title, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startBrowser(url: String, title: String, html: String?) {
|
||||||
rhinoContext.ensureActive()
|
rhinoContext.ensureActive()
|
||||||
SourceVerificationHelp.startBrowser(getSource(), url, title)
|
SourceVerificationHelp.startBrowser(getSource(), url, title, html = html)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用内置浏览器打开链接,并等待网页结果
|
* 使用内置浏览器打开链接,并等待网页结果
|
||||||
*/
|
*/
|
||||||
fun startBrowserAwait(url: String, title: String, refetchAfterSuccess: Boolean): StrResponse {
|
fun startBrowserAwait(url: String, title: String): StrResponse {
|
||||||
rhinoContext.ensureActive()
|
return startBrowserAwait(url, title, true, null)
|
||||||
val body = SourceVerificationHelp.getVerificationResult(
|
|
||||||
getSource(), url, title, true, refetchAfterSuccess
|
|
||||||
)
|
|
||||||
return StrResponse(url, body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startBrowserAwait(url: String, title: String): StrResponse {
|
fun startBrowserAwait(url: String, title: String, refetchAfterSuccess: Boolean): StrResponse {
|
||||||
return startBrowserAwait(url, title, true)
|
return startBrowserAwait(url, title, refetchAfterSuccess, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startBrowserAwait(
|
||||||
|
url: String,
|
||||||
|
title: String,
|
||||||
|
refetchAfterSuccess: Boolean,
|
||||||
|
html: String?
|
||||||
|
): StrResponse {
|
||||||
|
rhinoContext.ensureActive()
|
||||||
|
val pair = SourceVerificationHelp.getVerificationResult(
|
||||||
|
getSource(), url, title, true, refetchAfterSuccess, html
|
||||||
|
)
|
||||||
|
val (url2, body) = pair
|
||||||
|
return StrResponse(url2.ifEmpty { url }, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -255,12 +344,13 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
*/
|
*/
|
||||||
fun getVerificationCode(imageUrl: String): String {
|
fun getVerificationCode(imageUrl: String): String {
|
||||||
rhinoContext.ensureActive()
|
rhinoContext.ensureActive()
|
||||||
return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
|
return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false).second
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可从网络,本地文件(阅读私有数据目录相对路径)导入JavaScript脚本
|
* 可从网络,本地文件(阅读私有数据目录相对路径)导入JavaScript脚本
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun importScript(path: String): String {
|
fun importScript(path: String): String {
|
||||||
val result = when {
|
val result = when {
|
||||||
path.startsWith("http") -> cacheFile(path)
|
path.startsWith("http") -> cacheFile(path)
|
||||||
@@ -275,6 +365,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param urlStr 网络文件的链接
|
* @param urlStr 网络文件的链接
|
||||||
* @return 返回缓存后的文件内容
|
* @return 返回缓存后的文件内容
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun cacheFile(urlStr: String): String {
|
fun cacheFile(urlStr: String): String {
|
||||||
return cacheFile(urlStr, 0)
|
return cacheFile(urlStr, 0)
|
||||||
}
|
}
|
||||||
@@ -283,6 +374,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* 缓存以文本方式保存的文件 如.js .txt等
|
* 缓存以文本方式保存的文件 如.js .txt等
|
||||||
* @param saveTime 缓存时间,单位:秒
|
* @param saveTime 缓存时间,单位:秒
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun cacheFile(urlStr: String, saveTime: Int): String {
|
fun cacheFile(urlStr: String, saveTime: Int): String {
|
||||||
val key = md5Encode16(urlStr)
|
val key = md5Encode16(urlStr)
|
||||||
val cachePath = CacheManager.get(key)
|
val cachePath = CacheManager.get(key)
|
||||||
@@ -302,10 +394,12 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
*js实现读取cookie
|
*js实现读取cookie
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun getCookie(tag: String): String {
|
fun getCookie(tag: String): String {
|
||||||
return getCookie(tag, null)
|
return getCookie(tag, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun getCookie(tag: String, key: String?): String {
|
fun getCookie(tag: String, key: String?): String {
|
||||||
return if (key != null) {
|
return if (key != null) {
|
||||||
CookieStore.getKey(tag, key)
|
CookieStore.getKey(tag, key)
|
||||||
@@ -319,8 +413,9 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param url 下载地址:可带参数type
|
* @param url 下载地址:可带参数type
|
||||||
* @return 下载的文件相对路径
|
* @return 下载的文件相对路径
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun downloadFile(url: String): String {
|
fun downloadFile(url: String): String {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
|
val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
|
||||||
val type = analyzeUrl.type ?: UrlUtil.getSuffix(url)
|
val type = analyzeUrl.type ?: UrlUtil.getSuffix(url)
|
||||||
val path = FileUtils.getPath(
|
val path = FileUtils.getPath(
|
||||||
@@ -354,8 +449,9 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
"Deprecated",
|
"Deprecated",
|
||||||
ReplaceWith("downloadFile(url)")
|
ReplaceWith("downloadFile(url)")
|
||||||
)
|
)
|
||||||
|
@JavascriptInterface
|
||||||
fun downloadFile(content: String, url: String): String {
|
fun downloadFile(content: String, url: String): String {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
val type = AnalyzeUrl(url, source = getSource(), coroutineContext = context).type
|
val type = AnalyzeUrl(url, source = getSource(), coroutineContext = context).type
|
||||||
?: return ""
|
?: return ""
|
||||||
val path = FileUtils.getPath(
|
val path = FileUtils.getPath(
|
||||||
@@ -376,14 +472,19 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* js实现重定向拦截,网络访问get
|
* js实现重定向拦截,网络访问get
|
||||||
*/
|
*/
|
||||||
fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
|
fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
|
||||||
|
return get(urlStr, headers, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(urlStr: String, headers: Map<String, String>, timeout: Int?): Connection.Response {
|
||||||
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
||||||
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
||||||
} else headers
|
} else headers
|
||||||
val rateLimiter = ConcurrentRateLimiter(getSource())
|
val rateLimiter = ConcurrentRateLimiter(getSource())
|
||||||
val response = rateLimiter.withLimitBlocking {
|
val response = rateLimiter.withLimitBlocking {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
Jsoup.connect(urlStr)
|
Jsoup.connect(urlStr)
|
||||||
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
||||||
|
.timeout(timeout ?: 30000)
|
||||||
.ignoreContentType(true)
|
.ignoreContentType(true)
|
||||||
.followRedirects(false)
|
.followRedirects(false)
|
||||||
.headers(requestHeaders)
|
.headers(requestHeaders)
|
||||||
@@ -397,14 +498,19 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* js实现重定向拦截,网络访问head,不返回Response Body更省流量
|
* js实现重定向拦截,网络访问head,不返回Response Body更省流量
|
||||||
*/
|
*/
|
||||||
fun head(urlStr: String, headers: Map<String, String>): Connection.Response {
|
fun head(urlStr: String, headers: Map<String, String>): Connection.Response {
|
||||||
|
return head(urlStr, headers, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun head(urlStr: String, headers: Map<String, String>, timeout: Int?): Connection.Response {
|
||||||
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
||||||
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
||||||
} else headers
|
} else headers
|
||||||
val rateLimiter = ConcurrentRateLimiter(getSource())
|
val rateLimiter = ConcurrentRateLimiter(getSource())
|
||||||
val response = rateLimiter.withLimitBlocking {
|
val response = rateLimiter.withLimitBlocking {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
Jsoup.connect(urlStr)
|
Jsoup.connect(urlStr)
|
||||||
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
||||||
|
.timeout(timeout ?: 30000)
|
||||||
.ignoreContentType(true)
|
.ignoreContentType(true)
|
||||||
.followRedirects(false)
|
.followRedirects(false)
|
||||||
.headers(requestHeaders)
|
.headers(requestHeaders)
|
||||||
@@ -418,14 +524,24 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* 网络访问post
|
* 网络访问post
|
||||||
*/
|
*/
|
||||||
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
|
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
|
||||||
|
return post(urlStr, body, headers, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun post(
|
||||||
|
urlStr: String,
|
||||||
|
body: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
timeout: Int?
|
||||||
|
): Connection.Response {
|
||||||
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
val requestHeaders = if (getSource()?.enabledCookieJar == true) {
|
||||||
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
headers.toMutableMap().apply { put(cookieJarHeader, "1") }
|
||||||
} else headers
|
} else headers
|
||||||
val rateLimiter = ConcurrentRateLimiter(getSource())
|
val rateLimiter = ConcurrentRateLimiter(getSource())
|
||||||
val response = rateLimiter.withLimitBlocking {
|
val response = rateLimiter.withLimitBlocking {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
Jsoup.connect(urlStr)
|
Jsoup.connect(urlStr)
|
||||||
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
||||||
|
.timeout(timeout ?: 30000)
|
||||||
.ignoreContentType(true)
|
.ignoreContentType(true)
|
||||||
.followRedirects(false)
|
.followRedirects(false)
|
||||||
.requestBody(body)
|
.requestBody(body)
|
||||||
@@ -457,14 +573,17 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* js实现base64解码,不能删
|
* js实现base64解码,不能删
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun base64Decode(str: String?): String {
|
fun base64Decode(str: String?): String {
|
||||||
return Base64.decodeStr(str)
|
return Base64.decodeStr(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun base64Decode(str: String?, charset: String): String {
|
fun base64Decode(str: String?, charset: String): String {
|
||||||
return Base64.decodeStr(str, charset(charset))
|
return Base64.decodeStr(str, charset(charset))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun base64Decode(str: String, flags: Int): String {
|
fun base64Decode(str: String, flags: Int): String {
|
||||||
return EncoderUtils.base64Decode(str, flags)
|
return EncoderUtils.base64Decode(str, flags)
|
||||||
}
|
}
|
||||||
@@ -483,10 +602,12 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
return EncoderUtils.base64DecodeToByteArray(str, flags)
|
return EncoderUtils.base64DecodeToByteArray(str, flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun base64Encode(str: String): String? {
|
fun base64Encode(str: String): String? {
|
||||||
return EncoderUtils.base64Encode(str, 2)
|
return EncoderUtils.base64Encode(str, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun base64Encode(str: String, flags: Int): String? {
|
fun base64Encode(str: String, flags: Int): String? {
|
||||||
return EncoderUtils.base64Encode(str, flags)
|
return EncoderUtils.base64Encode(str, flags)
|
||||||
}
|
}
|
||||||
@@ -497,11 +618,13 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* hexString 解码为utf8String*/
|
/* hexString 解码为utf8String*/
|
||||||
|
@JavascriptInterface
|
||||||
fun hexDecodeToString(hex: String): String? {
|
fun hexDecodeToString(hex: String): String? {
|
||||||
return HexUtil.decodeHexStr(hex)
|
return HexUtil.decodeHexStr(hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* utf8 编码为hexString */
|
/* utf8 编码为hexString */
|
||||||
|
@JavascriptInterface
|
||||||
fun hexEncodeToString(utf8: String): String? {
|
fun hexEncodeToString(utf8: String): String? {
|
||||||
return HexUtil.encodeHexStr(utf8)
|
return HexUtil.encodeHexStr(utf8)
|
||||||
}
|
}
|
||||||
@@ -509,6 +632,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* 格式化时间
|
* 格式化时间
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun timeFormatUTC(time: Long, format: String, sh: Int): String? {
|
fun timeFormatUTC(time: Long, format: String, sh: Int): String? {
|
||||||
val utc = SimpleTimeZone(sh, "UTC")
|
val utc = SimpleTimeZone(sh, "UTC")
|
||||||
return SimpleDateFormat(format, Locale.getDefault()).run {
|
return SimpleDateFormat(format, Locale.getDefault()).run {
|
||||||
@@ -520,10 +644,12 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* 时间格式化
|
* 时间格式化
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun timeFormat(time: Long): String {
|
fun timeFormat(time: Long): String {
|
||||||
return dateFormat.format(Date(time))
|
return dateFormat.format(Date(time))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun encodeURI(str: String): String {
|
fun encodeURI(str: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLEncoder.encode(str, "UTF-8")
|
URLEncoder.encode(str, "UTF-8")
|
||||||
@@ -532,6 +658,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun encodeURI(str: String, enc: String): String {
|
fun encodeURI(str: String, enc: String): String {
|
||||||
return try {
|
return try {
|
||||||
URLEncoder.encode(str, enc)
|
URLEncoder.encode(str, enc)
|
||||||
@@ -540,18 +667,22 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun htmlFormat(str: String): String {
|
fun htmlFormat(str: String): String {
|
||||||
return HtmlFormatter.formatKeepImg(str)
|
return HtmlFormatter.formatKeepImg(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun t2s(text: String): String {
|
fun t2s(text: String): String {
|
||||||
return ChineseUtils.t2s(text)
|
return ChineseUtils.t2s(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun s2t(text: String): String {
|
fun s2t(text: String): String {
|
||||||
return ChineseUtils.s2t(text)
|
return ChineseUtils.s2t(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun getWebViewUA(): String {
|
fun getWebViewUA(): String {
|
||||||
return WebSettings.getDefaultUserAgent(appCtx)
|
return WebSettings.getDefaultUserAgent(appCtx)
|
||||||
}
|
}
|
||||||
@@ -586,6 +717,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun readTxtFile(path: String): String {
|
fun readTxtFile(path: String): String {
|
||||||
val file = getFile(path)
|
val file = getFile(path)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
@@ -595,6 +727,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun readTxtFile(path: String, charsetName: String): String {
|
fun readTxtFile(path: String, charsetName: String): String {
|
||||||
val file = getFile(path)
|
val file = getFile(path)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
@@ -606,6 +739,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* 删除本地文件
|
* 删除本地文件
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun deleteFile(path: String): Boolean {
|
fun deleteFile(path: String): Boolean {
|
||||||
val file = getFile(path)
|
val file = getFile(path)
|
||||||
return FileUtils.delete(file, true)
|
return FileUtils.delete(file, true)
|
||||||
@@ -616,6 +750,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param zipPath 相对路径
|
* @param zipPath 相对路径
|
||||||
* @return 相对路径
|
* @return 相对路径
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun unzipFile(zipPath: String): String {
|
fun unzipFile(zipPath: String): String {
|
||||||
return unArchiveFile(zipPath)
|
return unArchiveFile(zipPath)
|
||||||
}
|
}
|
||||||
@@ -625,6 +760,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param zipPath 相对路径
|
* @param zipPath 相对路径
|
||||||
* @return 相对路径
|
* @return 相对路径
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun un7zFile(zipPath: String): String {
|
fun un7zFile(zipPath: String): String {
|
||||||
return unArchiveFile(zipPath)
|
return unArchiveFile(zipPath)
|
||||||
}
|
}
|
||||||
@@ -634,6 +770,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param zipPath 相对路径
|
* @param zipPath 相对路径
|
||||||
* @return 相对路径
|
* @return 相对路径
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun unrarFile(zipPath: String): String {
|
fun unrarFile(zipPath: String): String {
|
||||||
return unArchiveFile(zipPath)
|
return unArchiveFile(zipPath)
|
||||||
}
|
}
|
||||||
@@ -643,6 +780,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param zipPath 相对路径
|
* @param zipPath 相对路径
|
||||||
* @return 相对路径
|
* @return 相对路径
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun unArchiveFile(zipPath: String): String {
|
fun unArchiveFile(zipPath: String): String {
|
||||||
if (zipPath.isEmpty()) return ""
|
if (zipPath.isEmpty()) return ""
|
||||||
val zipFile = getFile(zipPath)
|
val zipFile = getFile(zipPath)
|
||||||
@@ -656,6 +794,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param path 文件夹相对路径
|
* @param path 文件夹相对路径
|
||||||
* @return 所有文件字符串换行连接
|
* @return 所有文件字符串换行连接
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun getTxtInFolder(path: String): String {
|
fun getTxtInFolder(path: String): String {
|
||||||
if (path.isEmpty()) return ""
|
if (path.isEmpty()) return ""
|
||||||
val folder = getFile(path)
|
val folder = getFile(path)
|
||||||
@@ -680,12 +819,14 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param path 所需获取文件在zip内的路径
|
* @param path 所需获取文件在zip内的路径
|
||||||
* @return zip指定文件的数据
|
* @return zip指定文件的数据
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun getZipStringContent(url: String, path: String): String {
|
fun getZipStringContent(url: String, path: String): String {
|
||||||
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
||||||
val charsetName = EncodingDetect.getEncode(byteArray)
|
val charsetName = EncodingDetect.getEncode(byteArray)
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun getZipStringContent(url: String, path: String, charsetName: String): String {
|
fun getZipStringContent(url: String, path: String, charsetName: String): String {
|
||||||
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
@@ -697,12 +838,14 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param path 所需获取文件在zip内的路径
|
* @param path 所需获取文件在zip内的路径
|
||||||
* @return zip指定文件的数据
|
* @return zip指定文件的数据
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun getRarStringContent(url: String, path: String): String {
|
fun getRarStringContent(url: String, path: String): String {
|
||||||
val byteArray = getRarByteArrayContent(url, path) ?: return ""
|
val byteArray = getRarByteArrayContent(url, path) ?: return ""
|
||||||
val charsetName = EncodingDetect.getEncode(byteArray)
|
val charsetName = EncodingDetect.getEncode(byteArray)
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun getRarStringContent(url: String, path: String, charsetName: String): String {
|
fun getRarStringContent(url: String, path: String, charsetName: String): String {
|
||||||
val byteArray = getRarByteArrayContent(url, path) ?: return ""
|
val byteArray = getRarByteArrayContent(url, path) ?: return ""
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
@@ -714,12 +857,14 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* @param path 所需获取文件在7zip内的路径
|
* @param path 所需获取文件在7zip内的路径
|
||||||
* @return zip指定文件的数据
|
* @return zip指定文件的数据
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun get7zStringContent(url: String, path: String): String {
|
fun get7zStringContent(url: String, path: String): String {
|
||||||
val byteArray = get7zByteArrayContent(url, path) ?: return ""
|
val byteArray = get7zByteArrayContent(url, path) ?: return ""
|
||||||
val charsetName = EncodingDetect.getEncode(byteArray)
|
val charsetName = EncodingDetect.getEncode(byteArray)
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun get7zStringContent(url: String, path: String, charsetName: String): String {
|
fun get7zStringContent(url: String, path: String, charsetName: String): String {
|
||||||
val byteArray = get7zByteArrayContent(url, path) ?: return ""
|
val byteArray = get7zByteArrayContent(url, path) ?: return ""
|
||||||
return String(byteArray, Charset.forName(charsetName))
|
return String(byteArray, Charset.forName(charsetName))
|
||||||
@@ -913,6 +1058,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* 章节数转数字
|
* 章节数转数字
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun toNumChapter(s: String?): String? {
|
fun toNumChapter(s: String?): String? {
|
||||||
s ?: return null
|
s ?: return null
|
||||||
val matcher = AppPattern.titleNumPattern.matcher(s)
|
val matcher = AppPattern.titleNumPattern.matcher(s)
|
||||||
@@ -956,7 +1102,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
getSource()?.let {
|
getSource()?.let {
|
||||||
Debug.log(it.getKey(), msg.toString())
|
Debug.log(it.getKey(), msg.toString())
|
||||||
} ?: Debug.log(msg.toString())
|
} ?: Debug.log(msg.toString())
|
||||||
AppLog.putDebug("${getSource()?.getTag() ?: "源"}调试输出: $msg")
|
AppLog.putDebug("${getTag() ?: "源"}调试输出: $msg")
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -974,22 +1120,32 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
/**
|
/**
|
||||||
* 生成UUID
|
* 生成UUID
|
||||||
*/
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
fun randomUUID(): String {
|
fun randomUUID(): String {
|
||||||
return UUID.randomUUID().toString()
|
return UUID.randomUUID().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun androidId(): String {
|
fun androidId(): String {
|
||||||
return AppConst.androidId
|
return AppConst.androidId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun openUrl(url: String) {
|
fun openUrl(url: String) {
|
||||||
openUrl(url, null)
|
openUrl(url, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新增 mimeType 参数,默认为 null(保持兼容性)
|
// 新增 mimeType 参数,默认为 null(保持兼容性)
|
||||||
|
@JavascriptInterface
|
||||||
fun openUrl(url: String, mimeType: String? = null) {
|
fun openUrl(url: String, mimeType: String? = null) {
|
||||||
require(url.length < 64 * 1024) { "openUrl parameter url too long" }
|
require(url.length < 64 * 1024) { "openUrl parameter url too long" }
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
|
if (url.startsWith("legado://") || url.startsWith("yuedu://")) {
|
||||||
|
appCtx.startActivity<OnLineImportActivity> {
|
||||||
|
data = url.toUri()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null")
|
val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null")
|
||||||
appCtx.startActivity<OpenUrlConfirmActivity> {
|
appCtx.startActivity<OpenUrlConfirmActivity> {
|
||||||
putExtra("uri", url)
|
putExtra("uri", url)
|
||||||
@@ -1000,4 +1156,37 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取阅读配置
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getReadBookConfig(): String {
|
||||||
|
return GSON.toJson(ReadBookConfig.durConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fun getReadBookConfigMap(): Map<String, Any> {
|
||||||
|
return ReadBookConfig.durConfig.toMap()
|
||||||
|
} */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题模式
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
fun getThemeMode(): String {
|
||||||
|
return AppConfig.themeMode ?: "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主题配置
|
||||||
|
*/
|
||||||
|
//@JavascriptInterface
|
||||||
|
/*fun getThemeConfig(): String {
|
||||||
|
val themeConfig = ThemeConfig.getDurConfig(appCtx)
|
||||||
|
return GSON.toJson(themeConfig)
|
||||||
|
}*/
|
||||||
|
|
||||||
|
/* fun getThemeConfigMap(): Map<String, Any?> {
|
||||||
|
return ThemeConfig.getDurConfig(appCtx).toMap()
|
||||||
|
} */
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -566,6 +566,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
appCtx.putPrefBoolean(PreferKey.previewImageByClick, value)
|
appCtx.putPrefBoolean(PreferKey.previewImageByClick, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val clickImgWay: String?
|
||||||
|
get() = appCtx.getPrefString(PreferKey.clickImgWay)
|
||||||
var preDownloadNum
|
var preDownloadNum
|
||||||
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
||||||
set(value) {
|
set(value) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class StrResponse {
|
|||||||
private set
|
private set
|
||||||
var errorBody: ResponseBody? = null
|
var errorBody: ResponseBody? = null
|
||||||
private set
|
private set
|
||||||
|
var callTime = 0
|
||||||
|
|
||||||
constructor(rawResponse: Response, body: String?) {
|
constructor(rawResponse: Response, body: String?) {
|
||||||
this.raw = rawResponse
|
this.raw = rawResponse
|
||||||
@@ -46,7 +47,11 @@ class StrResponse {
|
|||||||
this.errorBody = errorBody
|
this.errorBody = errorBody
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun putCallTime(callTime: Int) {
|
||||||
|
this.callTime = callTime
|
||||||
|
}
|
||||||
fun raw() = raw
|
fun raw() = raw
|
||||||
|
fun callTime() = callTime
|
||||||
|
|
||||||
fun url(): String {
|
fun url(): String {
|
||||||
raw.networkResponse?.let {
|
raw.networkResponse?.let {
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ object SourceVerificationHelp {
|
|||||||
url: String,
|
url: String,
|
||||||
title: String,
|
title: String,
|
||||||
useBrowser: Boolean,
|
useBrowser: Boolean,
|
||||||
refetchAfterSuccess: Boolean = true
|
refetchAfterSuccess: Boolean = true,
|
||||||
): String {
|
html: String? = null
|
||||||
|
): Pair<String, String> {
|
||||||
source
|
source
|
||||||
?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
|
?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
|
||||||
require(url.length < 64 * 1024) { "getVerificationResult parameter url too long" }
|
require(url.length < 64 * 1024) { "getVerificationResult parameter url too long" }
|
||||||
@@ -53,23 +54,21 @@ object SourceVerificationHelp {
|
|||||||
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
|
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
startBrowser(source, url, title, true, refetchAfterSuccess)
|
startBrowser(source, url, title, true, refetchAfterSuccess, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitUserInput = false
|
var waitUserInput = false
|
||||||
while (getResult(source.getKey()) == null) {
|
while (getResult(source.getKey()) == null) {
|
||||||
if (!waitUserInput) {
|
if (!waitUserInput && html == null) {
|
||||||
AppLog.putDebug("等待返回验证结果...")
|
AppLog.putDebug("等待返回验证结果...")
|
||||||
waitUserInput = true
|
waitUserInput = true
|
||||||
}
|
}
|
||||||
LockSupport.parkNanos(this, waitTime)
|
LockSupport.parkNanos(this, waitTime)
|
||||||
}
|
}
|
||||||
|
val result = getResult(source.getKey()) ?: throw NoStackTraceException("验证结果为空")
|
||||||
return getResult(source.getKey())!!.let {
|
clearResult(source.getKey())
|
||||||
it.ifBlank {
|
if (result.second.isEmpty()) throw NoStackTraceException("验证结果为空")
|
||||||
throw NoStackTraceException("验证结果为空")
|
return result
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,7 +80,8 @@ object SourceVerificationHelp {
|
|||||||
url: String,
|
url: String,
|
||||||
title: String,
|
title: String,
|
||||||
saveResult: Boolean? = false,
|
saveResult: Boolean? = false,
|
||||||
refetchAfterSuccess: Boolean? = true
|
refetchAfterSuccess: Boolean? = true,
|
||||||
|
html: String? = null
|
||||||
) {
|
) {
|
||||||
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
|
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
|
||||||
require(url.length < 64 * 1024) { "startBrowser parameter url too long" }
|
require(url.length < 64 * 1024) { "startBrowser parameter url too long" }
|
||||||
@@ -93,6 +93,7 @@ object SourceVerificationHelp {
|
|||||||
putExtra("sourceType", source.getSourceType())
|
putExtra("sourceType", source.getSourceType())
|
||||||
putExtra("sourceVerificationEnable", saveResult)
|
putExtra("sourceVerificationEnable", saveResult)
|
||||||
putExtra("refetchAfterSuccess", refetchAfterSuccess)
|
putExtra("refetchAfterSuccess", refetchAfterSuccess)
|
||||||
|
putExtra("html", html)
|
||||||
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
|
IntentData.put(getVerificationResultKey(source), Thread.currentThread())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,12 +105,17 @@ object SourceVerificationHelp {
|
|||||||
LockSupport.unpark(thread)
|
LockSupport.unpark(thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setResult(sourceKey: String, result: String?) {
|
fun setResult(sourceKey: String, result: String, url: String = "") {
|
||||||
CacheManager.putMemory(getVerificationResultKey(sourceKey), result ?: "")
|
CacheManager.putMemory(getVerificationResultKey(sourceKey), (url to result))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getResult(sourceKey: String): String? {
|
fun getResult(sourceKey: String): Pair<String, String>? {
|
||||||
return CacheManager.get(getVerificationResultKey(sourceKey))
|
val pair = CacheManager.getFromMemory(getVerificationResultKey(sourceKey)) as? Pair<*, *>
|
||||||
|
?: return null
|
||||||
|
if (pair.first is String && pair.second is String) {
|
||||||
|
return pair.first as String to pair.second as String
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearResult(sourceKey: String) {
|
fun clearResult(sourceKey: String) {
|
||||||
|
|||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package io.legado.app.help.webView
|
||||||
|
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.WebView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
|
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.help.coroutine.Coroutine
|
||||||
|
import io.legado.app.model.AudioPlay
|
||||||
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||||
|
import io.legado.app.ui.rss.read.RssJsExtensions
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.escapeForJs
|
||||||
|
import io.legado.app.utils.fromJsonObject
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class WebJsExtensions(
|
||||||
|
source: BaseSource,
|
||||||
|
activity: AppCompatActivity,
|
||||||
|
private val webView: WebView,
|
||||||
|
private val bookType: Int = 0
|
||||||
|
) : RssJsExtensions(activity, source) {
|
||||||
|
private val bookAndChapter by lazy {
|
||||||
|
var book: Book? = null
|
||||||
|
var chapter: BookChapter? = null
|
||||||
|
when (bookType) {
|
||||||
|
BookType.text -> {
|
||||||
|
book = ReadBook.book?.also {
|
||||||
|
chapter = appDb.bookChapterDao.getChapter(
|
||||||
|
it.bookUrl,
|
||||||
|
ReadBook.durChapterIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookType.audio -> {
|
||||||
|
book = AudioPlay.book
|
||||||
|
chapter = AudioPlay.durChapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pair(book, chapter)
|
||||||
|
}
|
||||||
|
private val book: Book? get() = bookAndChapter.first
|
||||||
|
private val chapter: BookChapter? get() = bookAndChapter.second
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由软件主动注入的js函数调用
|
||||||
|
*/
|
||||||
|
@JavascriptInterface
|
||||||
|
fun request(funName: String, jsParam: Array<String>, id: String) {
|
||||||
|
val activity = activityRef.get() ?: return
|
||||||
|
Coroutine.async(activity.lifecycleScope) {
|
||||||
|
when (funName) {
|
||||||
|
"run" -> AnalyzeRule(book, getSource()).run {
|
||||||
|
setCoroutineContext(coroutineContext)
|
||||||
|
setChapter(chapter)
|
||||||
|
evalJS(jsParam[0]).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
"ajaxAwait" -> {
|
||||||
|
ajax(jsParam[0], jsParam[1].toIntOrNull()).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
"connectAwait" -> {
|
||||||
|
connect(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
"getAwait" -> {
|
||||||
|
get(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
"headAwait" -> {
|
||||||
|
head(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
"postAwait" -> {
|
||||||
|
post(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toIntOrNull())
|
||||||
|
}
|
||||||
|
|
||||||
|
"webViewAwait" -> {
|
||||||
|
webView(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toBoolean()).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
"decryptStrAwait" -> {
|
||||||
|
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).decryptStr(jsParam[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
"encryptBase64Await" -> {
|
||||||
|
createSymmetricCrypto(
|
||||||
|
jsParam[0],
|
||||||
|
jsParam[1],
|
||||||
|
jsParam[2]
|
||||||
|
).encryptBase64(jsParam[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
"encryptHexAwait" -> {
|
||||||
|
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).encryptHex(jsParam[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
"createSignHexAwait" -> {
|
||||||
|
createSign(jsParam[0]).setPublicKey(jsParam[1]).setPrivateKey(jsParam[2])
|
||||||
|
.signHex(jsParam[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
"downloadFileAwait" -> {
|
||||||
|
downloadFile(jsParam[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
"readTxtFileAwait" -> {
|
||||||
|
readTxtFile(jsParam[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
"importScriptAwait" -> {
|
||||||
|
importScript(jsParam[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
"getStringAwait" -> AnalyzeRule(book, getSource()).run {
|
||||||
|
setCoroutineContext(coroutineContext)
|
||||||
|
setChapter(chapter)
|
||||||
|
getString(jsParam[0], jsParam[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> "error funName"
|
||||||
|
}
|
||||||
|
}.onSuccess { data ->
|
||||||
|
webView.evaluateJavascript(
|
||||||
|
"window.$JSBridgeResult('$id', '${data.escapeForJs()}', null);",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}.onError {
|
||||||
|
webView.evaluateJavascript(
|
||||||
|
"window.$JSBridgeResult('$id', null, '${it.localizedMessage?.escapeForJs()}');",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun toast(msg: String?) {
|
||||||
|
super.toast(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun longToast(msg: String?) {
|
||||||
|
super.longToast(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun log(msg: String?): String {
|
||||||
|
return super.log(msg).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun ajax(url: String): String? {
|
||||||
|
return super.ajax(url, 9000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun ajax(url: String, callTimeout: Int?): String? {
|
||||||
|
return super.ajax(url, callTimeout?.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun connect(urlStr: String?): String {
|
||||||
|
if (urlStr.isNullOrEmpty()) return "error empty url"
|
||||||
|
return super.connect(urlStr, null, 9000).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun connect(urlStr: String, header: String): String {
|
||||||
|
return super.connect(urlStr, header, 9000).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun connect(urlStr: String, header: String, callTimeout: Int?): String {
|
||||||
|
return super.connect(urlStr, header, callTimeout?.toLong()).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun get(urlStr: String, headers: String): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return super.get(urlStr, headerMap, 9000).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun get(urlStr: String, headers: String, timeout: Int?): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return super.get(urlStr, headerMap, timeout).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun post(urlStr: String, body: String, headers: String): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return super.post(urlStr, body, headerMap, 9000).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun post(urlStr: String, body: String, headers: String, timeout: Int?): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return super.post(urlStr, body, headerMap, timeout).body()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun head(urlStr: String, headers: String): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return GSON.toJson(super.head(urlStr, headerMap, 9000).headers())
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun head(urlStr: String, headers: String, timeout: Int?): String {
|
||||||
|
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||||
|
return GSON.toJson(super.head(urlStr, headerMap, timeout).headers())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun getRandomLetter(): Char {
|
||||||
|
val letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_"
|
||||||
|
return letters.random()
|
||||||
|
}
|
||||||
|
|
||||||
|
val uuid by lazy { UUID.randomUUID().toString().split("-") }
|
||||||
|
val uuid2 by lazy { UUID.randomUUID().toString().split("-") }
|
||||||
|
val nameJava by lazy { getRandomLetter() + uuid[0] + uuid[1] }
|
||||||
|
val nameCache by lazy { getRandomLetter() + uuid[2] + uuid[3] }
|
||||||
|
val nameSource by lazy { getRandomLetter() + uuid[4] }
|
||||||
|
val nameBasic by lazy { getRandomLetter() + uuid2[1] + uuid2[2] }
|
||||||
|
val JSBridgeResult by lazy { getRandomLetter() + uuid2[3] + uuid2[4] }
|
||||||
|
val JS_INJECTION by lazy {
|
||||||
|
"""
|
||||||
|
const requestId = n => 'req_' + n + '_' + Date.now() + '_' + Math.random().toString(36).slice(-3);
|
||||||
|
const JSBridgeCallbacks = {};
|
||||||
|
const java = window.$nameJava;
|
||||||
|
const source = window.$nameSource;
|
||||||
|
const cache = window.$nameCache;
|
||||||
|
function run(jsCode) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("run");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("run", [String(jsCode)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function ajaxAwait(url, callTimeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("ajaxAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("ajaxAwait", [String(url), String(callTimeout)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function connectAwait(url, header, callTimeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("connectAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("connectAwait", [String(url), String(header), String(callTimeout)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function getAwait(url, header, callTimeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("getAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("getAwait", [String(url), String(header), String(callTimeout)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function headAwait(url, header, callTimeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("headAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("headAwait", [String(url), String(header), String(callTimeout)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function postAwait(url, body, header, callTimeout) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("postAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("postAwait", [String(url), String(body), String(header), String(callTimeout)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function webViewAwait(html, url, js, cacheFirst) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("webViewAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("webViewAwait", [String(html), String(url), String(js), String(cacheFirst)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function decryptStrAwait(transformation, key, iv, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("decryptStrAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("decryptStrAwait", [String(transformation), String(key), String(iv), String(data)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function encryptBase64Await(transformation, key, iv, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("encryptBase64Await");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("encryptBase64Await", [String(transformation), String(key), String(iv), String(data)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function encryptHexAwait(transformation, key, iv, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("encryptHexAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("encryptHexAwait", [String(transformation), String(key), String(iv), String(data)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function createSignHexAwait(algorithm, publicKey, privateKey, data) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("createSignHexAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("createSignHexAwait", [String(algorithm), String(publicKey), String(privateKey), String(data)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function downloadFileAwait(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("downloadFileAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("downloadFileAwait", [String(url)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function readTxtFileAwait(path) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("readTxtFileAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("readTxtFileAwait", [String(path)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function importScriptAwait(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("importScriptAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("importScriptAwait", [String(url)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function getStringAwait(ruleStr, mContent) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = requestId("getStringAwait");
|
||||||
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
|
window.$nameJava?.request("getStringAwait", [String(ruleStr), String(mContent)], id);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.$JSBridgeResult = function(requestId, result, error) {
|
||||||
|
if (JSBridgeCallbacks[requestId]) {
|
||||||
|
if (error) {
|
||||||
|
JSBridgeCallbacks[requestId].reject(error);
|
||||||
|
} else {
|
||||||
|
JSBridgeCallbacks[requestId].resolve(result);
|
||||||
|
}
|
||||||
|
delete JSBridgeCallbacks[requestId];
|
||||||
|
}
|
||||||
|
};"""
|
||||||
|
}
|
||||||
|
|
||||||
|
val basicJs by lazy {
|
||||||
|
"""
|
||||||
|
(function() {
|
||||||
|
if (screen.orientation && !screen.orientation.__patched) {
|
||||||
|
screen.orientation.lock = function(orientation) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
window.$nameBasic?.lockOrientation(orientation)
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
screen.orientation.unlock = function() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
window.$nameBasic?.lockOrientation('unlock')
|
||||||
|
resolve()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
screen.orientation.__patched = true;
|
||||||
|
};
|
||||||
|
window.close = function() {
|
||||||
|
window.$nameBasic?.onCloseRequested();
|
||||||
|
};
|
||||||
|
})();"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,8 @@ import kotlin.coroutines.EmptyCoroutineContext
|
|||||||
class AnalyzeRule(
|
class AnalyzeRule(
|
||||||
private var ruleData: RuleDataInterface? = null,
|
private var ruleData: RuleDataInterface? = null,
|
||||||
private val source: BaseSource? = null,
|
private val source: BaseSource? = null,
|
||||||
private val preUpdateJs: Boolean = false
|
private val preUpdateJs: Boolean = false,
|
||||||
|
private var isFromBookInfo: Boolean = false
|
||||||
) : JsExtensions {
|
) : JsExtensions {
|
||||||
|
|
||||||
private val book get() = ruleData as? BaseBook
|
private val book get() = ruleData as? BaseBook
|
||||||
@@ -79,6 +80,12 @@ class AnalyzeRule(
|
|||||||
private var coroutineContext: CoroutineContext = EmptyCoroutineContext
|
private var coroutineContext: CoroutineContext = EmptyCoroutineContext
|
||||||
|
|
||||||
private var loggedNonStandardJSON = false
|
private var loggedNonStandardJSON = false
|
||||||
|
private var ruleName: String? = null
|
||||||
|
fun setRuleName(name: String) {
|
||||||
|
if (name.isNotBlank()) {
|
||||||
|
ruleName = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule {
|
fun setContent(content: Any?, baseUrl: String? = null): AnalyzeRule {
|
||||||
@@ -782,6 +789,7 @@ class AnalyzeRule(
|
|||||||
bindings["src"] = content
|
bindings["src"] = content
|
||||||
bindings["nextChapterUrl"] = nextChapterUrl
|
bindings["nextChapterUrl"] = nextChapterUrl
|
||||||
bindings["rssArticle"] = rssArticle
|
bindings["rssArticle"] = rssArticle
|
||||||
|
bindings["fromBookInfo"] = isFromBookInfo
|
||||||
}
|
}
|
||||||
val topScope = source?.getShareScope(coroutineContext) ?: topScopeRef?.get()
|
val topScope = source?.getShareScope(coroutineContext) ?: topScopeRef?.get()
|
||||||
val scope = if (topScope == null) {
|
val scope = if (topScope == null) {
|
||||||
@@ -810,6 +818,10 @@ class AnalyzeRule(
|
|||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTag(): String? {
|
||||||
|
return source?.getTag() ?: ruleName
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* js实现跨域访问,不能删
|
* js实现跨域访问,不能删
|
||||||
*/
|
*/
|
||||||
@@ -841,6 +853,9 @@ class AnalyzeRule(
|
|||||||
*/
|
*/
|
||||||
fun reGetBook() {
|
fun reGetBook() {
|
||||||
if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用")
|
if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用")
|
||||||
|
if (isFromBookInfo) {
|
||||||
|
log("重新获取book")
|
||||||
|
}
|
||||||
val bookSource = source as? BookSource
|
val bookSource = source as? BookSource
|
||||||
val book = book as? Book
|
val book = book as? Book
|
||||||
if (bookSource == null || book == null) return
|
if (bookSource == null || book == null) return
|
||||||
@@ -863,6 +878,10 @@ class AnalyzeRule(
|
|||||||
*/
|
*/
|
||||||
fun refreshTocUrl() {
|
fun refreshTocUrl() {
|
||||||
if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用")
|
if (!preUpdateJs) throw NoStackTraceException("只能在 preUpdateJs 中调用")
|
||||||
|
if (isFromBookInfo) {
|
||||||
|
log("已跳过重复加载详情页,请优化代码")
|
||||||
|
return
|
||||||
|
}
|
||||||
val bookSource = source as? BookSource
|
val bookSource = source as? BookSource
|
||||||
val book = book as? Book
|
val book = book as? Book
|
||||||
if (bookSource == null || book == null) return
|
if (bookSource == null || book == null) return
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import com.script.rhino.RhinoScriptEngine
|
|||||||
import com.script.rhino.runScriptWithContext
|
import com.script.rhino.runScriptWithContext
|
||||||
import io.legado.app.constant.AppConst.UA_NAME
|
import io.legado.app.constant.AppConst.UA_NAME
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.AppPattern.JS_PATTERN
|
|
||||||
import io.legado.app.constant.AppPattern.dataUriRegex
|
|
||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
@@ -50,15 +48,21 @@ import io.legado.app.utils.isJson
|
|||||||
import io.legado.app.utils.isJsonArray
|
import io.legado.app.utils.isJsonArray
|
||||||
import io.legado.app.utils.isJsonObject
|
import io.legado.app.utils.isJsonObject
|
||||||
import io.legado.app.utils.isXml
|
import io.legado.app.utils.isXml
|
||||||
|
import io.legado.app.utils.parseIpsFromString
|
||||||
|
import io.legado.app.utils.stackTraceStr
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import okhttp3.Dns
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import kotlin.coroutines.ContinuationInterceptor
|
import kotlin.coroutines.ContinuationInterceptor
|
||||||
@@ -87,8 +91,10 @@ class AnalyzeUrl(
|
|||||||
private val callTimeout: Long? = null,
|
private val callTimeout: Long? = null,
|
||||||
private var coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
private var coroutineContext: CoroutineContext = EmptyCoroutineContext,
|
||||||
headerMapF: Map<String, String>? = null,
|
headerMapF: Map<String, String>? = null,
|
||||||
hasLoginHeader: Boolean = true
|
hasLoginHeader: Boolean = true,
|
||||||
|
private val infoMap: MutableMap<String, String>? = null
|
||||||
) : JsExtensions {
|
) : JsExtensions {
|
||||||
|
constructor(mUrl: String) : this(mUrl, null)
|
||||||
|
|
||||||
var ruleUrl = ""
|
var ruleUrl = ""
|
||||||
private set
|
private set
|
||||||
@@ -98,7 +104,8 @@ class AnalyzeUrl(
|
|||||||
private set
|
private set
|
||||||
val headerMap = LinkedHashMap<String, String>()
|
val headerMap = LinkedHashMap<String, String>()
|
||||||
private var body: String? = null
|
private var body: String? = null
|
||||||
private var urlNoQuery: String = ""
|
var urlNoQuery: String = ""
|
||||||
|
private set
|
||||||
private var encodedForm: String? = null
|
private var encodedForm: String? = null
|
||||||
private var encodedQuery: String? = null
|
private var encodedQuery: String? = null
|
||||||
private var charset: String? = null
|
private var charset: String? = null
|
||||||
@@ -107,6 +114,8 @@ class AnalyzeUrl(
|
|||||||
private var retry: Int = 0
|
private var retry: Int = 0
|
||||||
private var useWebView: Boolean = false
|
private var useWebView: Boolean = false
|
||||||
private var webJs: String? = null
|
private var webJs: String? = null
|
||||||
|
private var bodyJs: String? = null
|
||||||
|
private var dnsIp: String? = null
|
||||||
private val enabledCookieJar = source?.enabledCookieJar == true
|
private val enabledCookieJar = source?.enabledCookieJar == true
|
||||||
private val domain: String
|
private val domain: String
|
||||||
private var webViewDelayTime: Long = 0
|
private var webViewDelayTime: Long = 0
|
||||||
@@ -151,7 +160,7 @@ class AnalyzeUrl(
|
|||||||
*/
|
*/
|
||||||
private fun analyzeJs() {
|
private fun analyzeJs() {
|
||||||
var start = 0
|
var start = 0
|
||||||
val jsMatcher = JS_PATTERN.matcher(ruleUrl)
|
val jsMatcher = AppPattern.JS_PATTERN.matcher(ruleUrl)
|
||||||
var result = ruleUrl
|
var result = ruleUrl
|
||||||
while (jsMatcher.find()) {
|
while (jsMatcher.find()) {
|
||||||
if (jsMatcher.start() > start) {
|
if (jsMatcher.start() > start) {
|
||||||
@@ -184,9 +193,9 @@ class AnalyzeUrl(
|
|||||||
//替换所有内嵌{{js}}
|
//替换所有内嵌{{js}}
|
||||||
val url = analyze.innerRule("{{", "}}") {
|
val url = analyze.innerRule("{{", "}}") {
|
||||||
val jsEval = evalJS(it) ?: ""
|
val jsEval = evalJS(it) ?: ""
|
||||||
when {
|
when (jsEval) {
|
||||||
jsEval is String -> jsEval
|
is String -> jsEval
|
||||||
jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval)
|
is Double if jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval)
|
||||||
else -> jsEval.toString()
|
else -> jsEval.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,15 +251,17 @@ class AnalyzeUrl(
|
|||||||
retry = option.getRetry()
|
retry = option.getRetry()
|
||||||
useWebView = option.useWebView()
|
useWebView = option.useWebView()
|
||||||
webJs = option.getWebJs()
|
webJs = option.getWebJs()
|
||||||
|
bodyJs = option.getBodyJs()
|
||||||
|
dnsIp = option.getDnsIp()
|
||||||
option.getJs()?.let { jsStr ->
|
option.getJs()?.let { jsStr ->
|
||||||
evalJS(jsStr, url)?.toString()?.let {
|
evalJS(jsStr, url)?.toString()?.let {
|
||||||
url = it
|
url = it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
serverID = option.getServerID()
|
serverID = option.getServerID()
|
||||||
webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0)
|
webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
urlNoQuery = url
|
urlNoQuery = url
|
||||||
when (method) {
|
when (method) {
|
||||||
RequestMethod.GET -> {
|
RequestMethod.GET -> {
|
||||||
@@ -341,6 +352,7 @@ class AnalyzeUrl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行JS
|
* 执行JS
|
||||||
*/
|
*/
|
||||||
@@ -357,6 +369,7 @@ class AnalyzeUrl(
|
|||||||
bindings["book"] = ruleData as? Book
|
bindings["book"] = ruleData as? Book
|
||||||
bindings["source"] = source
|
bindings["source"] = source
|
||||||
bindings["result"] = result
|
bindings["result"] = result
|
||||||
|
bindings["infoMap"] = infoMap
|
||||||
}
|
}
|
||||||
val sharedScope = source?.getShareScope(coroutineContext)
|
val sharedScope = source?.getShareScope(coroutineContext)
|
||||||
val scope = if (sharedScope == null) {
|
val scope = if (sharedScope == null) {
|
||||||
@@ -397,13 +410,30 @@ class AnalyzeUrl(
|
|||||||
jsStr: String? = null,
|
jsStr: String? = null,
|
||||||
sourceRegex: String? = null,
|
sourceRegex: String? = null,
|
||||||
useWebView: Boolean = true,
|
useWebView: Boolean = true,
|
||||||
|
isTest: Boolean = false,
|
||||||
|
skipRateLimit: Boolean = false
|
||||||
): StrResponse {
|
): StrResponse {
|
||||||
if (type != null) {
|
if (type != null) {
|
||||||
return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait()))
|
return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait()))
|
||||||
}
|
}
|
||||||
|
if (skipRateLimit) {
|
||||||
|
return executeStrRequest(jsStr, sourceRegex, useWebView, isTest)
|
||||||
|
}
|
||||||
concurrentRateLimiter.withLimit {
|
concurrentRateLimiter.withLimit {
|
||||||
|
return executeStrRequest(jsStr, sourceRegex, useWebView, isTest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun executeStrRequest(
|
||||||
|
jsStr: String? = null,
|
||||||
|
sourceRegex: String? = null,
|
||||||
|
useWebView: Boolean = true,
|
||||||
|
isTest: Boolean = false
|
||||||
|
): StrResponse {
|
||||||
setCookie()
|
setCookie()
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
val strResponse: StrResponse
|
val strResponse: StrResponse
|
||||||
|
try {
|
||||||
if (this.useWebView && useWebView) {
|
if (this.useWebView && useWebView) {
|
||||||
strResponse = when (method) {
|
strResponse = when (method) {
|
||||||
RequestMethod.POST -> {
|
RequestMethod.POST -> {
|
||||||
@@ -461,10 +491,36 @@ class AnalyzeUrl(
|
|||||||
?.matches(AppPattern.xmlContentTypeRegex) == true
|
?.matches(AppPattern.xmlContentTypeRegex) == true
|
||||||
if (isXml && it.body?.trim()?.startsWith("<?xml", true) == false) {
|
if (isXml && it.body?.trim()?.startsWith("<?xml", true) == false) {
|
||||||
StrResponse(it.raw, "<?xml version=\"1.0\"?>" + it.body)
|
StrResponse(it.raw, "<?xml version=\"1.0\"?>" + it.body)
|
||||||
|
} else if (bodyJs != null) {
|
||||||
|
val body = evalJS(bodyJs!!, it.body).toString()
|
||||||
|
StrResponse(it.raw, body)
|
||||||
} else it
|
} else it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val connectionTime = System.currentTimeMillis() - startTime
|
||||||
|
strResponse.putCallTime(connectionTime.toInt())
|
||||||
return strResponse
|
return strResponse
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (!isTest) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
val errorCode = when (e) {
|
||||||
|
is java.net.SocketTimeoutException -> -2 // 超时错误
|
||||||
|
is java.net.UnknownHostException -> -3 // 未找到域名
|
||||||
|
is java.net.ConnectException -> -4 // 连接被拒绝
|
||||||
|
is java.net.SocketException -> -5 // Socket错误(包括连接重置)
|
||||||
|
is javax.net.ssl.SSLException -> -6 // SSL证书或握手错误
|
||||||
|
is java.io.InterruptedIOException -> {
|
||||||
|
if (e.message?.contains("timeout") == true) {
|
||||||
|
-1 // 超过设定时间
|
||||||
|
} else -7
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> -7 // 其它错误
|
||||||
|
}
|
||||||
|
return StrResponse(url, e.message).apply {
|
||||||
|
putCallTime(errorCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,11 +565,31 @@ class AnalyzeUrl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回一个errResponse
|
||||||
|
*/
|
||||||
|
fun getErrResponse(e: Throwable): Response = Response.Builder()
|
||||||
|
.request(Request.Builder().url(url).build())
|
||||||
|
.protocol(okhttp3.Protocol.HTTP_1_1)
|
||||||
|
.code(500)
|
||||||
|
.message(e.message ?: "Error Response")
|
||||||
|
.body(e.stackTraceStr.toResponseBody(null))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回一个errStrResponse
|
||||||
|
*/
|
||||||
|
fun getErrStrResponse(e: Throwable): StrResponse =
|
||||||
|
StrResponse(getErrResponse(e), e.stackTraceStr)
|
||||||
|
|
||||||
private fun getClient(): OkHttpClient {
|
private fun getClient(): OkHttpClient {
|
||||||
val client = getProxyClient(proxy)
|
val client = getProxyClient(proxy)
|
||||||
if (readTimeout == null && callTimeout == null) {
|
if (readTimeout == null && callTimeout == null && dnsIp == null) {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
if (AppConfig.isCronet && dnsIp != null) {
|
||||||
|
customIp[urlNoQuery] = dnsIp!!
|
||||||
|
}
|
||||||
return client.newBuilder().run {
|
return client.newBuilder().run {
|
||||||
if (readTimeout != null) {
|
if (readTimeout != null) {
|
||||||
readTimeout(readTimeout, TimeUnit.MILLISECONDS)
|
readTimeout(readTimeout, TimeUnit.MILLISECONDS)
|
||||||
@@ -522,10 +598,21 @@ class AnalyzeUrl(
|
|||||||
if (callTimeout != null) {
|
if (callTimeout != null) {
|
||||||
callTimeout(callTimeout, TimeUnit.MILLISECONDS)
|
callTimeout(callTimeout, TimeUnit.MILLISECONDS)
|
||||||
}
|
}
|
||||||
|
if (dnsIp != null) {
|
||||||
|
val inetAddress = dnsIp!!.parseIpsFromString()
|
||||||
|
dns { hostname ->
|
||||||
|
inetAddress ?: Dns.SYSTEM.lookup(hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
build()
|
build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun extractHostFromUrl(url: String): String? {
|
||||||
|
return AppPattern.domainRegex.find(url)?.groupValues?.getOrNull(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun getResponse(): Response {
|
fun getResponse(): Response {
|
||||||
return runBlocking(coroutineContext) {
|
return runBlocking(coroutineContext) {
|
||||||
getResponseAwait()
|
getResponseAwait()
|
||||||
@@ -536,7 +623,7 @@ class AnalyzeUrl(
|
|||||||
if (!urlNoQuery.startsWith("data:")) {
|
if (!urlNoQuery.startsWith("data:")) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val dataUriFindResult = dataUriRegex.find(urlNoQuery)
|
val dataUriFindResult = AppPattern.dataUriRegex.find(urlNoQuery)
|
||||||
if (dataUriFindResult != null) {
|
if (dataUriFindResult != null) {
|
||||||
val dataUriBase64 = dataUriFindResult.groupValues[1]
|
val dataUriBase64 = dataUriFindResult.groupValues[1]
|
||||||
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
|
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
|
||||||
@@ -666,12 +753,16 @@ class AnalyzeUrl(
|
|||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTag(): String? {
|
||||||
|
return source?.getTag()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val paramPattern: Pattern = Pattern.compile("\\s*,\\s*(?=\\{)")
|
val paramPattern: Pattern = Pattern.compile("\\s*,\\s*(?=\\{)")
|
||||||
private val pagePattern = Pattern.compile("<(.*?)>")
|
private val pagePattern = Pattern.compile("<(.*?)>")
|
||||||
private val queryEncoder =
|
private val queryEncoder =
|
||||||
RFC3986.UNRESERVED.orNew(PercentCodec.of("!$%&()*+,/:;=?@[\\]^`{|}"))
|
RFC3986.UNRESERVED.orNew(PercentCodec.of("!$%&()*+,/:;=?@[\\]^`{|}"))
|
||||||
|
val customIp by lazy { ConcurrentHashMap<String, String>() }
|
||||||
fun AnalyzeUrl.getMediaItem(): MediaItem {
|
fun AnalyzeUrl.getMediaItem(): MediaItem {
|
||||||
setCookie()
|
setCookie()
|
||||||
return ExoPlayerHelper.createMediaItem(url, headerMap)
|
return ExoPlayerHelper.createMediaItem(url, headerMap)
|
||||||
@@ -705,11 +796,20 @@ class AnalyzeUrl(
|
|||||||
* webView中执行的js
|
* webView中执行的js
|
||||||
**/
|
**/
|
||||||
private var webJs: String? = null,
|
private var webJs: String? = null,
|
||||||
|
/**
|
||||||
|
* 自定义的域名ip
|
||||||
|
**/
|
||||||
|
private var dnsIp: String? = null,
|
||||||
/**
|
/**
|
||||||
* 解析完url参数时执行的js
|
* 解析完url参数时执行的js
|
||||||
* 执行结果会赋值给url
|
* 执行结果会赋值给url
|
||||||
*/
|
*/
|
||||||
private var js: String? = null,
|
private var js: String? = null,
|
||||||
|
/**
|
||||||
|
* 得到访问结果后执行的js,对结果进行二次处理
|
||||||
|
* 执行结果返回为body
|
||||||
|
*/
|
||||||
|
private var bodyJs: String? = null,
|
||||||
/**
|
/**
|
||||||
* 服务器id
|
* 服务器id
|
||||||
*/
|
*/
|
||||||
@@ -808,6 +908,13 @@ class AnalyzeUrl(
|
|||||||
fun getWebJs(): String? {
|
fun getWebJs(): String? {
|
||||||
return webJs
|
return webJs
|
||||||
}
|
}
|
||||||
|
fun setDnsIp(value: String?) {
|
||||||
|
dnsIp = if (value.isNullOrBlank()) null else value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDnsIp(): String? {
|
||||||
|
return dnsIp
|
||||||
|
}
|
||||||
|
|
||||||
fun setJs(value: String?) {
|
fun setJs(value: String?) {
|
||||||
js = if (value.isNullOrBlank()) null else value
|
js = if (value.isNullOrBlank()) null else value
|
||||||
@@ -817,6 +924,14 @@ class AnalyzeUrl(
|
|||||||
return js
|
return js
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setBodyJs(value: String?) {
|
||||||
|
bodyJs = if (value.isNullOrBlank()) null else value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBodyJs(): String? {
|
||||||
|
return bodyJs
|
||||||
|
}
|
||||||
|
|
||||||
fun setServerID(value: String?) {
|
fun setServerID(value: String?) {
|
||||||
serverID = if (value.isNullOrBlank()) null else value.toLong()
|
serverID = if (value.isNullOrBlank()) null else value.toLong()
|
||||||
}
|
}
|
||||||
@@ -835,14 +950,18 @@ class AnalyzeUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ConcurrentRecord(
|
data class ConcurrentRecord(
|
||||||
/**
|
|
||||||
* 是否按频率
|
|
||||||
*/
|
|
||||||
val isConcurrent: Boolean,
|
|
||||||
/**
|
/**
|
||||||
* 开始访问时间
|
* 开始访问时间
|
||||||
*/
|
*/
|
||||||
var time: Long,
|
var time: Long,
|
||||||
|
/**
|
||||||
|
* 限制次数
|
||||||
|
*/
|
||||||
|
var accessLimit: Int,
|
||||||
|
/**
|
||||||
|
* 间隔时间
|
||||||
|
*/
|
||||||
|
var interval: Int,
|
||||||
/**
|
/**
|
||||||
* 正在访问的个数
|
* 正在访问的个数
|
||||||
*/
|
*/
|
||||||
@@ -850,3 +969,4 @@ class AnalyzeUrl(
|
|||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||||
|
import com.script.rhino.runScriptWithContext
|
||||||
import io.legado.app.BuildConfig
|
import io.legado.app.BuildConfig
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
@@ -63,6 +64,7 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.paramPattern
|
||||||
import io.legado.app.model.localBook.EpubFile
|
import io.legado.app.model.localBook.EpubFile
|
||||||
import io.legado.app.model.localBook.MobiFile
|
import io.legado.app.model.localBook.MobiFile
|
||||||
import io.legado.app.receiver.NetworkChangedListener
|
import io.legado.app.receiver.NetworkChangedListener
|
||||||
@@ -102,6 +104,7 @@ import io.legado.app.ui.book.toc.rule.TxtTocRuleDialog
|
|||||||
import io.legado.app.ui.browser.WebViewActivity
|
import io.legado.app.ui.browser.WebViewActivity
|
||||||
import io.legado.app.ui.dict.DictDialog
|
import io.legado.app.ui.dict.DictDialog
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
|
import io.legado.app.ui.login.SourceLoginJsExtensions
|
||||||
import io.legado.app.ui.replace.ReplaceEditRoute
|
import io.legado.app.ui.replace.ReplaceEditRoute
|
||||||
import io.legado.app.ui.replace.ReplaceRuleActivity
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
||||||
import io.legado.app.ui.widget.PopupAction
|
import io.legado.app.ui.widget.PopupAction
|
||||||
@@ -121,7 +124,6 @@ import io.legado.app.utils.hexString
|
|||||||
import io.legado.app.utils.iconItemOnLongClick
|
import io.legado.app.utils.iconItemOnLongClick
|
||||||
import io.legado.app.utils.invisible
|
import io.legado.app.utils.invisible
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
import io.legado.app.utils.isJsonObject
|
|
||||||
import io.legado.app.utils.isTrue
|
import io.legado.app.utils.isTrue
|
||||||
import io.legado.app.utils.navigationBarGravity
|
import io.legado.app.utils.navigationBarGravity
|
||||||
import io.legado.app.utils.observeEvent
|
import io.legado.app.utils.observeEvent
|
||||||
@@ -1521,45 +1523,75 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
showHelp("readMenuHelp")
|
showHelp("readMenuHelp")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clickImg(clickjs: String) {
|
/**
|
||||||
val braceIndex = clickjs.indexOf(",{")
|
* 点击图片
|
||||||
val braceIndex2 = if (braceIndex == -1) clickjs.indexOf(", {") else -1
|
*/
|
||||||
if (braceIndex != -1 || braceIndex2 != -1) {
|
override fun oldClickImg(src: String): Boolean {
|
||||||
val book = ReadBook.book ?: return
|
val urlMatcher = paramPattern.matcher(src)
|
||||||
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
if (urlMatcher.find()) {
|
||||||
if (chapter == null) {
|
val urlOptionStr = src.substring(urlMatcher.end())
|
||||||
toastOnUi("章节不存在")
|
val urlOptionMap = GSON.fromJsonObject<Map<String, String>>(urlOptionStr).getOrNull()
|
||||||
return
|
val click = urlOptionMap?.get("click")
|
||||||
}
|
if (click != null) {
|
||||||
val (result, urlOptionStr) = when {
|
Coroutine.async(lifecycleScope, IO) {
|
||||||
braceIndex != -1 -> {
|
val source = ReadBook.bookSource ?: return@async
|
||||||
clickjs.take(braceIndex) to clickjs.substring(braceIndex + 1)
|
val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text)
|
||||||
}
|
val book = ReadBook.book ?: return@async
|
||||||
else -> {
|
val chapter =
|
||||||
clickjs.take(braceIndex2) to clickjs.substring(braceIndex2 + 2)
|
appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
||||||
|
?: throw Exception("no find chapter")
|
||||||
|
runScriptWithContext {
|
||||||
|
source.evalJS(click) {
|
||||||
|
put("java", java)
|
||||||
|
put("book", book)
|
||||||
|
put("chapter", chapter)
|
||||||
|
put("result", src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (urlOptionStr.isJsonObject()) {
|
}.onError {
|
||||||
val urlOptionMap = GSON.fromJsonObject<Map<String, String>>(urlOptionStr).getOrThrow()
|
AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true)
|
||||||
val jsStr = urlOptionMap["js"]
|
}
|
||||||
jsStr?.let {
|
return true
|
||||||
Coroutine.async(lifecycleScope) {
|
}
|
||||||
val source = ReadBook.bookSource ?: throw Exception("书源不存在")
|
val jsStr = urlOptionMap?.get("js") ?: return false
|
||||||
|
Coroutine.async(lifecycleScope, IO) {
|
||||||
|
val source = ReadBook.bookSource ?: return@async
|
||||||
|
val book = ReadBook.book ?: return@async
|
||||||
|
val chapter =
|
||||||
|
appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
||||||
|
?: throw Exception("no find chapter")
|
||||||
|
val urlNoOption = src.take(urlMatcher.start())
|
||||||
AnalyzeRule(book, source).apply {
|
AnalyzeRule(book, source).apply {
|
||||||
setCoroutineContext(coroutineContext)
|
setCoroutineContext(coroutineContext)
|
||||||
setBaseUrl(chapter.url)
|
setBaseUrl(chapter.url)
|
||||||
setChapter(chapter)
|
setChapter(chapter)
|
||||||
evalJS(jsStr, result).toString()
|
evalJS(jsStr, urlNoOption)
|
||||||
}
|
}
|
||||||
}.onError {
|
}.onError {
|
||||||
AppLog.put("图片点击执行出错\n${it.localizedMessage}", it, true)
|
AppLog.put("执行图片链接js键值出错\n${it.localizedMessage}", it, true)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clickImg(click: String, src: String) {
|
||||||
|
Coroutine.async(lifecycleScope, IO) {
|
||||||
|
val source = ReadBook.bookSource ?: return@async
|
||||||
|
val java = SourceLoginJsExtensions(this@ReadBookActivity, source, BookType.text)
|
||||||
|
val book = ReadBook.book ?: return@async
|
||||||
|
val chapter = appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
||||||
|
?: throw Exception("no find chapter")
|
||||||
|
runScriptWithContext {
|
||||||
|
source.evalJS(click) {
|
||||||
|
put("java", java)
|
||||||
|
put("book", book)
|
||||||
|
put("chapter", chapter)
|
||||||
|
put("result", src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}.onError {
|
||||||
else {
|
AppLog.put("执行图片链接click键值出错\n${it.localizedMessage}", it, true)
|
||||||
toastOnUi("链接格式错误")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import io.legado.app.data.entities.Bookmark
|
|||||||
import io.legado.app.help.book.isOnLineTxt
|
import io.legado.app.help.book.isOnLineTxt
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.ui.association.OpenUrlConfirmActivity
|
||||||
import io.legado.app.ui.book.read.page.delegate.PageDelegate
|
import io.legado.app.ui.book.read.page.delegate.PageDelegate
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||||
@@ -20,6 +21,7 @@ import io.legado.app.ui.book.read.page.entities.column.ButtonColumn
|
|||||||
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
|
import io.legado.app.ui.book.read.page.entities.column.ImageColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.ReviewColumn
|
import io.legado.app.ui.book.read.page.entities.column.ReviewColumn
|
||||||
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
import io.legado.app.ui.book.read.page.entities.column.TextColumn
|
||||||
|
import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||||
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
import io.legado.app.ui.book.read.page.provider.TextPageFactory
|
||||||
import io.legado.app.ui.widget.dialog.PhotoDialog
|
import io.legado.app.ui.widget.dialog.PhotoDialog
|
||||||
@@ -27,6 +29,7 @@ import io.legado.app.utils.activity
|
|||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.getCompatColor
|
import io.legado.app.utils.getCompatColor
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
@@ -246,18 +249,51 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
|||||||
handled = true
|
handled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
is ImageColumn -> if (AppConfig.previewImageByClick) {
|
is ImageColumn -> when (AppConfig.clickImgWay) {
|
||||||
|
"1" -> { //预览图片
|
||||||
activity?.showDialogFragment(PhotoDialog(column.src))
|
activity?.showDialogFragment(PhotoDialog(column.src))
|
||||||
handled = true
|
handled = true
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
"2" -> { //兼容处理
|
||||||
if (ReadBook.book?.isOnLineTxt == true) {
|
if (ReadBook.book?.isOnLineTxt == true) {
|
||||||
|
val click = column.click
|
||||||
val src = column.src
|
val src = column.src
|
||||||
if (src.contains("\"js\"") || src.contains("'js'")) {
|
if (!click.isNullOrBlank()) {
|
||||||
callBack.clickImg(src)
|
callBack.clickImg(click, src)
|
||||||
|
handled = true
|
||||||
|
} else {
|
||||||
|
handled = callBack.oldClickImg(src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"3" -> { //关闭
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> { //默认点击
|
||||||
|
val click = column.click
|
||||||
|
if (!click.isNullOrBlank()) {
|
||||||
|
callBack.clickImg(click, column.src)
|
||||||
handled = true
|
handled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is TextHtmlColumn -> {
|
||||||
|
column.linkUrl?.let {
|
||||||
|
activity?.startActivity<OpenUrlConfirmActivity> {
|
||||||
|
putExtra("uri", it)
|
||||||
|
// putExtra("mimeType", mimeType)
|
||||||
|
// putExtra("sourceOrigin", source.getKey())
|
||||||
|
// putExtra("sourceName", source.getTag())
|
||||||
|
// putExtra("sourceType", source.getSourceType())
|
||||||
|
}
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return handled
|
return handled
|
||||||
@@ -723,6 +759,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
|
|||||||
fun onImageLongPress(x: Float, y: Float, src: String)
|
fun onImageLongPress(x: Float, y: Float, src: String)
|
||||||
fun onCancelSelect()
|
fun onCancelSelect()
|
||||||
fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean
|
fun onLongScreenshotTouchEvent(event: MotionEvent): Boolean
|
||||||
fun clickImg(clickjs: String)
|
fun oldClickImg(src: String): Boolean
|
||||||
|
fun clickImg(click: String, src: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
import io.legado.app.ui.book.read.page.ContentTextView
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
@@ -18,7 +19,8 @@ import splitties.init.appCtx
|
|||||||
data class ImageColumn(
|
data class ImageColumn(
|
||||||
override var start: Float,
|
override var start: Float,
|
||||||
override var end: Float,
|
override var end: Float,
|
||||||
var src: String
|
var src: String,
|
||||||
|
var click: String? = null
|
||||||
) : BaseColumn {
|
) : BaseColumn {
|
||||||
|
|
||||||
override var textLine: TextLine = emptyTextLine
|
override var textLine: TextLine = emptyTextLine
|
||||||
@@ -49,4 +51,8 @@ data class ImageColumn(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isTouch(x: Float): Boolean {
|
||||||
|
return x > start && x < end + 20.dpToPx()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ import io.legado.app.help.config.ReadBookConfig
|
|||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.ImageProvider
|
import io.legado.app.model.ImageProvider
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl.Companion.paramPattern
|
||||||
import io.legado.app.ui.book.read.page.entities.TextChapter
|
import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||||
@@ -37,9 +38,11 @@ import io.legado.app.ui.book.read.page.provider.ChapterProvider.reviewChar
|
|||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceChar
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceChar
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharC
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharC
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharD
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider.srcReplaceCharD
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.StringUtils
|
import io.legado.app.utils.StringUtils
|
||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.fastSum
|
import io.legado.app.utils.fastSum
|
||||||
|
import io.legado.app.utils.fromJsonObject
|
||||||
import io.legado.app.utils.getTextWidthsCompat
|
import io.legado.app.utils.getTextWidthsCompat
|
||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
@@ -340,6 +343,7 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
var start = 0
|
var start = 0
|
||||||
val srcList = LinkedList<String>()
|
val srcList = LinkedList<String>()
|
||||||
|
val clickList = LinkedList<String?>()
|
||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
var isFirstLine = true
|
var isFirstLine = true
|
||||||
if (content.contains("<img")) {
|
if (content.contains("<img")) {
|
||||||
@@ -347,51 +351,58 @@ class TextChapterLayout(
|
|||||||
while (matcher.find()) {
|
while (matcher.find()) {
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
val imgSrc = matcher.group(1)!!
|
val imgSrc = matcher.group(1)!!
|
||||||
var iStyle = imageStyle
|
var iStyle: String? = null
|
||||||
var isSmallImage = true
|
var click: String? = null
|
||||||
val matchResult = AppPattern.imgStyRegex.find(imgSrc)
|
var imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
||||||
var imgSize: Size? = null
|
val urlMatcher = paramPattern.matcher(imgSrc)
|
||||||
if (matchResult != null) {
|
if (urlMatcher.find()) {
|
||||||
val styleValue = matchResult.groupValues[1].trim()
|
var width: String? = null
|
||||||
if (styleValue.equals("text", true)) { //忽略大小写
|
val urlOptionStr = imgSrc.substring(urlMatcher.end())
|
||||||
iStyle = styleValue
|
GSON.fromJsonObject<Map<String, String>>(urlOptionStr).getOrNull()
|
||||||
} else {
|
?.let { map ->
|
||||||
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
map.forEach { (key, value) ->
|
||||||
iStyle = styleValue
|
when (key) {
|
||||||
isSmallImage = false
|
"style" -> iStyle = value
|
||||||
}
|
"width" -> width = value
|
||||||
} else {
|
"click" -> click = value
|
||||||
imgSize = ImageProvider.getImageSize(book, imgSrc, ReadBook.bookSource)
|
|
||||||
if (imgSize.width < 80 && imgSize.height < 80) {
|
|
||||||
iStyle = "text"
|
|
||||||
} else {
|
|
||||||
isSmallImage = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
width?.let {
|
||||||
|
if (it.endsWith("%")) {
|
||||||
|
it.dropLast(1).toIntOrNull()?.let { percentage ->
|
||||||
|
val imgWidth = visibleWidth * percentage / 100
|
||||||
|
val newHeight = imgSize.height * imgWidth / imgSize.width
|
||||||
|
imgSize = Size(imgWidth, newHeight)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it.toIntOrNull()?.let { w ->
|
||||||
|
val newHeight = imgSize.height * w / imgSize.width
|
||||||
|
imgSize = Size(w, newHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iStyle == null) {
|
||||||
|
iStyle =
|
||||||
|
if (imgSize.width < 80 && imgSize.height < 80) "text" else imageStyle
|
||||||
|
}
|
||||||
|
|
||||||
if (start < matcher.start()) {
|
if (start < matcher.start()) {
|
||||||
sb.append(text.substring(start, matcher.start()))
|
sb.append(text.substring(start, matcher.start()))
|
||||||
}
|
}
|
||||||
if (isSmallImage) {
|
if (iStyle == "text" || iStyle == "TEXT") {
|
||||||
sb.append(
|
sb.append(if (iStyle == "TEXT") reviewChar else srcReplaceChar)
|
||||||
if (iStyle == "TEXT")
|
|
||||||
reviewChar
|
|
||||||
else
|
|
||||||
srcReplaceChar
|
|
||||||
)
|
|
||||||
srcList.add(imgSrc)
|
srcList.add(imgSrc)
|
||||||
|
clickList.add(click)
|
||||||
} else {
|
} else {
|
||||||
val textBefore = sb.toString()
|
val textBefore = sb.toString()
|
||||||
if (textBefore.isNotBlank()) {
|
if (textBefore.isNotBlank()) {
|
||||||
wordCount += textBefore.replace(noWordCountRegex,"").length
|
wordCount += textBefore.replace(noWordCountRegex,"").length
|
||||||
setTypeText(
|
setTypeText(
|
||||||
book,
|
book, sb.toString(), contentPaint, contentPaintTextHeight,
|
||||||
sb.toString(),
|
contentPaintFontMetrics, "TEXT", isFirstLine = isFirstLine,
|
||||||
contentPaint,
|
srcList = srcList, clickList = clickList
|
||||||
contentPaintTextHeight,
|
|
||||||
contentPaintFontMetrics,
|
|
||||||
"TEXT",
|
|
||||||
isFirstLine = isFirstLine,
|
|
||||||
srcList = srcList
|
|
||||||
)
|
)
|
||||||
sb.setLength(0)
|
sb.setLength(0)
|
||||||
isFirstLine = false
|
isFirstLine = false
|
||||||
@@ -401,8 +412,9 @@ class TextChapterLayout(
|
|||||||
imgSrc,
|
imgSrc,
|
||||||
contentPaintTextHeight,
|
contentPaintTextHeight,
|
||||||
iStyle,
|
iStyle,
|
||||||
imgSize!!
|
imgSize,
|
||||||
)
|
click
|
||||||
|
) // 传递点击信息
|
||||||
isSetTypedImage = true
|
isSetTypedImage = true
|
||||||
}
|
}
|
||||||
start = matcher.end()
|
start = matcher.end()
|
||||||
@@ -427,7 +439,8 @@ class TextChapterLayout(
|
|||||||
contentPaintFontMetrics,
|
contentPaintFontMetrics,
|
||||||
"TEXT",
|
"TEXT",
|
||||||
isFirstLine = isFirstLine,
|
isFirstLine = isFirstLine,
|
||||||
srcList = srcList.ifEmpty { null }
|
srcList = srcList.ifEmpty { null },
|
||||||
|
clickList = clickList.ifEmpty { null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,7 +472,8 @@ class TextChapterLayout(
|
|||||||
src: String,
|
src: String,
|
||||||
textHeight: Float,
|
textHeight: Float,
|
||||||
imageStyle: String?,
|
imageStyle: String?,
|
||||||
size: Size
|
size: Size,
|
||||||
|
click: String? = null
|
||||||
) {
|
) {
|
||||||
if (size.width > 0 && size.height > 0) {
|
if (size.width > 0 && size.height > 0) {
|
||||||
prepareNextPageIfNeed(durY)
|
prepareNextPageIfNeed(durY)
|
||||||
@@ -776,7 +790,8 @@ class TextChapterLayout(
|
|||||||
isFirstLine: Boolean = true,
|
isFirstLine: Boolean = true,
|
||||||
emptyContent: Boolean = false,
|
emptyContent: Boolean = false,
|
||||||
isVolumeTitle: Boolean = false,
|
isVolumeTitle: Boolean = false,
|
||||||
srcList: LinkedList<String>? = null
|
srcList: LinkedList<String>? = null,
|
||||||
|
clickList: LinkedList<String?>? = null
|
||||||
) {
|
) {
|
||||||
val widthsArray = allocateFloatArray(text.length)
|
val widthsArray = allocateFloatArray(text.length)
|
||||||
textPaint.getTextWidthsCompat(text, widthsArray)
|
textPaint.getTextWidthsCompat(text, widthsArray)
|
||||||
@@ -836,7 +851,7 @@ class TextChapterLayout(
|
|||||||
//多行的第一行 非标题
|
//多行的第一行 非标题
|
||||||
addCharsToLineFirst(
|
addCharsToLineFirst(
|
||||||
book, absStartX, textLine, words, textPaint,
|
book, absStartX, textLine, words, textPaint,
|
||||||
desiredWidth, widths, srcList
|
desiredWidth, widths, srcList, clickList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -854,7 +869,7 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, !isTitle && lineIndex == 0, widths, srcList
|
startX, !isTitle && lineIndex == 0, widths, srcList, clickList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -867,13 +882,13 @@ class TextChapterLayout(
|
|||||||
val startX = (visibleWidth - desiredWidth) / 2
|
val startX = (visibleWidth - desiredWidth) / 2
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, false, widths, srcList
|
startX, false, widths, srcList, clickList
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
//中间行
|
//中间行
|
||||||
addCharsToLineMiddle(
|
addCharsToLineMiddle(
|
||||||
book, absStartX, textLine, words, textPaint,
|
book, absStartX, textLine, words, textPaint,
|
||||||
desiredWidth, 0f, widths, srcList
|
desiredWidth, 0f, widths, srcList, clickList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -926,13 +941,14 @@ class TextChapterLayout(
|
|||||||
/**自然排版长度**/
|
/**自然排版长度**/
|
||||||
desiredWidth: Float,
|
desiredWidth: Float,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?
|
srcList: LinkedList<String>?,
|
||||||
|
clickList: LinkedList<String?>?
|
||||||
) {
|
) {
|
||||||
var x = 0f
|
var x = 0f
|
||||||
if (!textFullJustify) {
|
if (!textFullJustify) {
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
x, true, textWidths, srcList
|
x, true, textWidths, srcList, clickList
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -955,7 +971,7 @@ class TextChapterLayout(
|
|||||||
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
||||||
addCharsToLineMiddle(
|
addCharsToLineMiddle(
|
||||||
book, absStartX, textLine, text1, textPaint,
|
book, absStartX, textLine, text1, textPaint,
|
||||||
desiredWidth, x, textWidths1, srcList
|
desiredWidth, x, textWidths1, srcList, clickList
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -974,12 +990,13 @@ class TextChapterLayout(
|
|||||||
/**起始x坐标**/
|
/**起始x坐标**/
|
||||||
startX: Float,
|
startX: Float,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?
|
srcList: LinkedList<String>?,
|
||||||
|
clickList: LinkedList<String?>?
|
||||||
) {
|
) {
|
||||||
if (!textFullJustify) {
|
if (!textFullJustify) {
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, false, textWidths, srcList
|
startX, false, textWidths, srcList, clickList
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1000,7 +1017,7 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
addCharToLine(
|
addCharToLine(
|
||||||
book, absStartX, textLine, char,
|
book, absStartX, textLine, char,
|
||||||
x, x1, index + 1 == words.size, srcList
|
x, x1, index + 1 == words.size, srcList, clickList
|
||||||
)
|
)
|
||||||
x = x1
|
x = x1
|
||||||
}
|
}
|
||||||
@@ -1016,7 +1033,7 @@ class TextChapterLayout(
|
|||||||
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
||||||
addCharToLine(
|
addCharToLine(
|
||||||
book, absStartX, textLine, char,
|
book, absStartX, textLine, char,
|
||||||
x, x1, index + 1 == words.size, srcList
|
x, x1, index + 1 == words.size, srcList, clickList
|
||||||
)
|
)
|
||||||
x = x1
|
x = x1
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1052,8 @@ class TextChapterLayout(
|
|||||||
startX: Float,
|
startX: Float,
|
||||||
hasIndent: Boolean,
|
hasIndent: Boolean,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?
|
srcList: LinkedList<String>?,
|
||||||
|
clickList: LinkedList<String?>?
|
||||||
) {
|
) {
|
||||||
val indentLength = paragraphIndent.length
|
val indentLength = paragraphIndent.length
|
||||||
var x = startX
|
var x = startX
|
||||||
@@ -1044,7 +1062,17 @@ class TextChapterLayout(
|
|||||||
val char = words[index]
|
val char = words[index]
|
||||||
val cw = textWidths[index]
|
val cw = textWidths[index]
|
||||||
val x1 = x + cw
|
val x1 = x + cw
|
||||||
addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
|
addCharToLine(
|
||||||
|
book,
|
||||||
|
absStartX,
|
||||||
|
textLine,
|
||||||
|
char,
|
||||||
|
x,
|
||||||
|
x1,
|
||||||
|
index + 1 == words.size,
|
||||||
|
srcList,
|
||||||
|
clickList
|
||||||
|
)
|
||||||
x = x1
|
x = x1
|
||||||
if (hasIndent && index == indentLength - 1) {
|
if (hasIndent && index == indentLength - 1) {
|
||||||
textLine.indentWidth = x
|
textLine.indentWidth = x
|
||||||
@@ -1064,16 +1092,19 @@ class TextChapterLayout(
|
|||||||
xStart: Float,
|
xStart: Float,
|
||||||
xEnd: Float,
|
xEnd: Float,
|
||||||
isLineEnd: Boolean,
|
isLineEnd: Boolean,
|
||||||
srcList: LinkedList<String>?
|
srcList: LinkedList<String>?,
|
||||||
|
clickList: LinkedList<String?>?
|
||||||
) {
|
) {
|
||||||
val column = when {
|
val column = when {
|
||||||
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
||||||
val src = srcList.removeFirst()
|
val src = srcList.removeFirst()
|
||||||
|
val click = clickList?.removeFirst()
|
||||||
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
|
ImageProvider.cacheImage(book, src, ReadBook.bookSource)
|
||||||
ImageColumn(
|
ImageColumn(
|
||||||
start = absStartX + xStart,
|
start = absStartX + xStart,
|
||||||
end = absStartX + xEnd,
|
end = absStartX + xEnd,
|
||||||
src = src
|
src = src,
|
||||||
|
click = click
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||||
|
|||||||
@@ -663,9 +663,10 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun start(context: Context, key: String?) {
|
fun start(context: Context, key: String?, searchScope: String? = null) {
|
||||||
context.startActivity<SearchActivity> {
|
context.startActivity<SearchActivity> {
|
||||||
putExtra("key", key)
|
putExtra("key", key)
|
||||||
|
putExtra("searchScope", searchScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
package io.legado.app.ui.login
|
package io.legado.app.ui.login
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
//import io.legado.app.lib.theme.primaryColor
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
|
import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.AppCompatSpinner
|
||||||
import androidx.core.view.setPadding
|
import androidx.core.view.setPadding
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
@@ -14,18 +24,25 @@ import io.legado.app.base.BaseBottomSheetDialogFragment
|
|||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.data.entities.rule.RowUi
|
import io.legado.app.data.entities.rule.RowUi
|
||||||
|
import io.legado.app.data.entities.rule.RowUi.Type
|
||||||
import io.legado.app.databinding.DialogLoginBinding
|
import io.legado.app.databinding.DialogLoginBinding
|
||||||
import io.legado.app.databinding.ItemFilletTextBinding
|
import io.legado.app.databinding.ItemFilletTextBinding
|
||||||
|
import io.legado.app.databinding.ItemSelectorSingleBinding
|
||||||
import io.legado.app.databinding.ItemSourceEditBinding
|
import io.legado.app.databinding.ItemSourceEditBinding
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.ui.about.AppLogDialog
|
import io.legado.app.ui.about.AppLogDialog
|
||||||
|
import io.legado.app.ui.widget.text.TextInputLayout
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.applyTint
|
import io.legado.app.utils.applyTint
|
||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
|
import io.legado.app.utils.fromJsonArray
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
|
import io.legado.app.utils.isTrue
|
||||||
import io.legado.app.utils.openUrl
|
import io.legado.app.utils.openUrl
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
import io.legado.app.utils.sendToClip
|
import io.legado.app.utils.sendToClip
|
||||||
|
import io.legado.app.utils.setLayout
|
||||||
|
import io.legado.app.utils.setSelectionSafely
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
@@ -35,72 +52,543 @@ import kotlinx.coroutines.ensureActive
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import splitties.views.onClick
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
|
||||||
class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
|
class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
|
||||||
|
|
||||||
private val binding by viewBinding(DialogLoginBinding::bind)
|
private val binding by viewBinding(DialogLoginBinding::bind)
|
||||||
private val viewModel by activityViewModels<SourceLoginViewModel>()
|
private val viewModel by activityViewModels<SourceLoginViewModel>()
|
||||||
|
private var lastClickTime: Long = 0
|
||||||
|
private var oKToClose = false
|
||||||
|
private var rowUis: List<RowUi>? = null
|
||||||
|
private var rowUiName = arrayListOf<String>()
|
||||||
|
private var hasChange = false
|
||||||
|
private val sourceLoginJsExtensions by lazy {
|
||||||
|
SourceLoginJsExtensions(
|
||||||
|
activity as AppCompatActivity,
|
||||||
|
viewModel.source,
|
||||||
|
viewModel.bookType,
|
||||||
|
callback = object : SourceLoginJsExtensions.Callback {
|
||||||
|
override fun upUiData(data: Map<String, String?>?) {
|
||||||
|
activity?.runOnUiThread { // 在主线程中更新 UI
|
||||||
|
handleUpUiData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
override fun reUiView() {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
handleReUiView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleReUiView() {
|
||||||
val source = viewModel.source ?: return
|
val source = viewModel.source ?: return
|
||||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
val loginUiStr = source.loginUi ?: return
|
||||||
binding.toolBar.title = getString(R.string.login_source, source.getTag())
|
val codeStr = loginUiStr.let {
|
||||||
val loginInfo = source.getLoginInfoMap()
|
when {
|
||||||
val loginUi = source.loginUi()
|
it.startsWith("@js:") -> it.substring(4)
|
||||||
try {
|
it.startsWith("<js>") -> it.substring(4, it.lastIndexOf("<"))
|
||||||
loginUi?.forEachIndexed { index, rowUi ->
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (codeStr != null) {
|
||||||
|
hasChange = true
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
val loginUiJson = evalUiJs(codeStr)
|
||||||
|
rowUis = loginUi(loginUiJson)
|
||||||
|
binding.flexbox.removeAllViews()
|
||||||
|
rowUiBuilder(source, rowUis)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowUis = loginUi(loginUiStr)
|
||||||
|
binding.flexbox.removeAllViews()
|
||||||
|
rowUiBuilder(source, rowUis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun handleUpUiData(data: Map<String, String?>?) {
|
||||||
|
hasChange = true
|
||||||
|
if (data == null) {
|
||||||
|
val newLoginInfo: MutableMap<String, String> = mutableMapOf()
|
||||||
|
rowUis?.forEachIndexed { index, rowUi ->
|
||||||
|
val default = rowUi.default
|
||||||
|
when (val rowView = binding.root.findViewById<View>(index + 1000)) {
|
||||||
|
is TextInputLayout -> {
|
||||||
|
newLoginInfo[rowUi.name] = default ?: ""
|
||||||
|
rowView.editText?.setText(default ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
is TextView -> {
|
||||||
when (rowUi.type) {
|
when (rowUi.type) {
|
||||||
RowUi.Type.text -> ItemSourceEditBinding.inflate(
|
Type.button -> {
|
||||||
layoutInflater,
|
rowView.text = rowUi.viewName ?: rowUi.name
|
||||||
binding.root,
|
|
||||||
false
|
|
||||||
).let {
|
|
||||||
binding.flexbox.addView(it.root)
|
|
||||||
it.root.id = index + 1000
|
|
||||||
it.textInputLayout.hint = rowUi.name
|
|
||||||
it.editText.setText(loginInfo?.get(rowUi.name))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RowUi.Type.password -> ItemSourceEditBinding.inflate(
|
Type.toggle -> {
|
||||||
|
val char = default ?: run {
|
||||||
|
val chars =
|
||||||
|
rowUi.chars?.filterNotNull() ?: listOf("chars is null")
|
||||||
|
chars.getOrNull(0) ?: ""
|
||||||
|
}
|
||||||
|
newLoginInfo[rowUi.name] = char
|
||||||
|
val name = rowUi.viewName ?: rowUi.name
|
||||||
|
val left = rowUi.style?.layout_justifySelf != "right"
|
||||||
|
rowView.text = if (left) char + name else name + char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is LinearLayout -> {
|
||||||
|
val chars = rowUi.chars?.filterNotNull() ?: listOf("chars", "is null")
|
||||||
|
val index = chars.indexOf(default)
|
||||||
|
newLoginInfo[rowUi.name] = default ?: run {
|
||||||
|
chars.getOrNull(0) ?: ""
|
||||||
|
}
|
||||||
|
rowView.findViewById<AppCompatSpinner>(R.id.sp_type)
|
||||||
|
?.setSelectionSafely(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.loginInfo = newLoginInfo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val loginInfo = viewModel.loginInfo
|
||||||
|
data.forEach { (key, value) ->
|
||||||
|
val index = rowUiName.indexOf(key)
|
||||||
|
if (index != -1) {
|
||||||
|
when (val rowView = binding.root.findViewById<View>(index + 1000)) {
|
||||||
|
is TextInputLayout -> {
|
||||||
|
val value = value ?: run {
|
||||||
|
val rowUi = rowUis?.getOrNull(index) ?: return@forEach
|
||||||
|
rowUi.default ?: ""
|
||||||
|
}
|
||||||
|
rowView.editText?.setText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
is TextView -> {
|
||||||
|
val rowUi = rowUis?.getOrNull(index) ?: return@forEach
|
||||||
|
when (rowUi.type) {
|
||||||
|
Type.button -> {
|
||||||
|
rowView.text = value ?: key
|
||||||
|
}
|
||||||
|
|
||||||
|
Type.toggle -> {
|
||||||
|
val char = value ?: run {
|
||||||
|
val chars =
|
||||||
|
rowUi.chars?.filterNotNull() ?: listOf("chars is null")
|
||||||
|
chars.getOrNull(0) ?: ""
|
||||||
|
}
|
||||||
|
loginInfo[rowUi.name] = char
|
||||||
|
val name = rowUi.viewName ?: rowUi.name
|
||||||
|
val left = rowUi.style?.layout_justifySelf != "right"
|
||||||
|
rowView.text = if (left) char + name else name + char
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is LinearLayout -> {
|
||||||
|
val rowUi = rowUis?.getOrNull(index) ?: return@forEach
|
||||||
|
val items = rowUi.chars?.filterNotNull() ?: listOf("chars", "is null")
|
||||||
|
val index = items.indexOf(value)
|
||||||
|
loginInfo[rowUi.name] = value ?: run {
|
||||||
|
items.getOrNull(0) ?: ""
|
||||||
|
}
|
||||||
|
rowView.findViewById<AppCompatSpinner>(R.id.sp_type)
|
||||||
|
?.setSelectionSafely(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loginInfo[key] = value ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun evalUiJs(jsStr: String): String? = withContext(IO) {
|
||||||
|
val source = viewModel.source ?: return@withContext null
|
||||||
|
val loginJS = source.getLoginJs() ?: ""
|
||||||
|
val result = rowUis?.let {
|
||||||
|
getLoginData(it)
|
||||||
|
} ?: viewModel.loginInfo.toMutableMap()
|
||||||
|
try {
|
||||||
|
runScriptWithContext {
|
||||||
|
source.evalJS("$loginJS\n$jsStr") {
|
||||||
|
put("result", result)
|
||||||
|
put("book", viewModel.book)
|
||||||
|
put("chapter", viewModel.chapter)
|
||||||
|
}.toString()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.put(source.getTag() + " loginUi err:" + (e.localizedMessage ?: e.toString()), e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loginUi(json: String?): List<RowUi>? {
|
||||||
|
return GSON.fromJsonArray<RowUi>(json).onFailure {
|
||||||
|
it.printOnDebug()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||||
|
private fun rowUiBuilder(source: BaseSource, rowUis: List<RowUi>?) {
|
||||||
|
val loginInfo = viewModel.loginInfo
|
||||||
|
rowUiName.clear()
|
||||||
|
rowUis?.forEachIndexed { index, rowUi ->
|
||||||
|
val type = rowUi.type
|
||||||
|
val name = rowUi.name
|
||||||
|
val viewName = rowUi.viewName
|
||||||
|
val action = rowUi.action
|
||||||
|
rowUiName.add(name)
|
||||||
|
when (type) {
|
||||||
|
Type.text -> ItemSourceEditBinding.inflate(
|
||||||
layoutInflater,
|
layoutInflater,
|
||||||
binding.root,
|
binding.root,
|
||||||
false
|
false
|
||||||
).let {
|
).let {
|
||||||
|
val editText = it.editText
|
||||||
binding.flexbox.addView(it.root)
|
binding.flexbox.addView(it.root)
|
||||||
|
rowUi.style().apply {
|
||||||
|
when (this.layout_justifySelf) {
|
||||||
|
"center" -> editText.gravity = Gravity.CENTER
|
||||||
|
"flex_end" -> editText.gravity = Gravity.END
|
||||||
|
}
|
||||||
|
apply(it.root)
|
||||||
|
}
|
||||||
it.root.id = index + 1000
|
it.root.id = index + 1000
|
||||||
it.textInputLayout.hint = rowUi.name
|
if (viewName == null) {
|
||||||
it.editText.inputType =
|
it.textInputLayout.hint = name
|
||||||
|
} else if (viewName.length in 3..19 && viewName.first() == '\'' && viewName.last() == '\'') {
|
||||||
|
it.textInputLayout.hint = viewName.substring(1, viewName.length - 1)
|
||||||
|
} else {
|
||||||
|
it.textInputLayout.hint = name
|
||||||
|
execute {
|
||||||
|
evalUiJs(viewName)
|
||||||
|
}.onSuccess { n ->
|
||||||
|
if (n.isNullOrEmpty()) {
|
||||||
|
it.textInputLayout.hint = "null"
|
||||||
|
} else {
|
||||||
|
it.textInputLayout.hint = n
|
||||||
|
}
|
||||||
|
}.onError { _ ->
|
||||||
|
it.textInputLayout.hint = "err"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editText.setText(loginInfo[name])
|
||||||
|
action?.let { jsStr ->
|
||||||
|
var content: String? = null
|
||||||
|
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
content = editText.text.toString()
|
||||||
|
} else {
|
||||||
|
val reContent = editText.text.toString()
|
||||||
|
if (content != reContent) {
|
||||||
|
execute {
|
||||||
|
evalUiJs(jsStr)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
if (result.isTrue()) {
|
||||||
|
loginInfo[name] = reContent
|
||||||
|
hasChange = true
|
||||||
|
}
|
||||||
|
}.onError { e ->
|
||||||
|
AppLog.put("LoginUI Text $name JavaScript error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editText.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
|
if (!editText.hasFocus()) {
|
||||||
|
return@addOnGlobalLayoutListener
|
||||||
|
}
|
||||||
|
val rect = Rect()
|
||||||
|
binding.root.getWindowVisibleDisplayFrame(rect)
|
||||||
|
val screenHeight = binding.root.height
|
||||||
|
val keypadHeight = screenHeight - rect.bottom
|
||||||
|
if (abs(keypadHeight) < screenHeight / 5) {
|
||||||
|
editText.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Type.password -> ItemSourceEditBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
binding.root,
|
||||||
|
false
|
||||||
|
).let {
|
||||||
|
val editText = it.editText
|
||||||
|
binding.flexbox.addView(it.root)
|
||||||
|
rowUi.style().apply {
|
||||||
|
when (this.layout_justifySelf) {
|
||||||
|
"center" -> editText.gravity = Gravity.CENTER
|
||||||
|
"flex_end" -> editText.gravity = Gravity.END
|
||||||
|
}
|
||||||
|
apply(it.root)
|
||||||
|
}
|
||||||
|
it.root.id = index + 1000
|
||||||
|
if (viewName == null) {
|
||||||
|
it.textInputLayout.hint = name
|
||||||
|
} else if (viewName.length in 3..19 && viewName.first() == '\'' && viewName.last() == '\'') {
|
||||||
|
it.textInputLayout.hint = viewName.substring(1, viewName.length - 1)
|
||||||
|
} else {
|
||||||
|
it.textInputLayout.hint = name
|
||||||
|
execute {
|
||||||
|
evalUiJs(viewName)
|
||||||
|
}.onSuccess { n ->
|
||||||
|
if (n.isNullOrEmpty()) {
|
||||||
|
it.textInputLayout.hint = "null"
|
||||||
|
} else {
|
||||||
|
it.textInputLayout.hint = n
|
||||||
|
}
|
||||||
|
}.onError { _ ->
|
||||||
|
it.textInputLayout.hint = "err"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editText.inputType =
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
||||||
it.editText.setText(loginInfo?.get(rowUi.name))
|
editText.setText(loginInfo[name])
|
||||||
|
action?.let { jsStr ->
|
||||||
|
var content: String? = null
|
||||||
|
editText.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus ->
|
||||||
|
if (hasFocus) {
|
||||||
|
content = editText.text.toString()
|
||||||
|
} else {
|
||||||
|
val reContent = editText.text.toString()
|
||||||
|
if (content != reContent) {
|
||||||
|
execute {
|
||||||
|
evalUiJs(jsStr)
|
||||||
|
}.onSuccess { result ->
|
||||||
|
if (result.isTrue()) {
|
||||||
|
loginInfo[name] = reContent
|
||||||
|
hasChange = true
|
||||||
|
}
|
||||||
|
}.onError { e ->
|
||||||
|
AppLog.put("LoginUI Text $name JavaScript error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editText.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
|
if (!editText.hasFocus()) {
|
||||||
|
return@addOnGlobalLayoutListener
|
||||||
|
}
|
||||||
|
val rect = Rect()
|
||||||
|
binding.root.getWindowVisibleDisplayFrame(rect)
|
||||||
|
val screenHeight = binding.root.height
|
||||||
|
val keypadHeight = screenHeight - rect.bottom
|
||||||
|
if (abs(keypadHeight) < screenHeight / 5) {
|
||||||
|
editText.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowUi.Type.button -> ItemFilletTextBinding.inflate(
|
Type.select -> ItemSelectorSingleBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
binding.root,
|
||||||
|
false
|
||||||
|
).let {
|
||||||
|
if (viewName == null) {
|
||||||
|
it.spName.text = name
|
||||||
|
} else if (viewName.length in 3..19 && viewName.first() == '\'' && viewName.last() == '\'') {
|
||||||
|
it.spName.text = viewName.substring(1, viewName.length - 1)
|
||||||
|
} else {
|
||||||
|
it.spName.text = name
|
||||||
|
execute {
|
||||||
|
evalUiJs(viewName)
|
||||||
|
}.onSuccess { n ->
|
||||||
|
if (n.isNullOrEmpty()) {
|
||||||
|
it.spName.text = "null"
|
||||||
|
} else {
|
||||||
|
it.spName.text = n
|
||||||
|
}
|
||||||
|
}.onError { _ ->
|
||||||
|
it.spName.text = "err"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val chars = rowUi.chars?.filterNotNull() ?: listOf("chars", "is null")
|
||||||
|
val adapter = ArrayAdapter(
|
||||||
|
requireContext(),
|
||||||
|
android.R.layout.simple_list_item_1,
|
||||||
|
chars
|
||||||
|
)
|
||||||
|
val selector = it.spType
|
||||||
|
selector.adapter = adapter
|
||||||
|
val infoV = loginInfo[name]
|
||||||
|
val char = if (infoV.isNullOrEmpty()) {
|
||||||
|
hasChange = true
|
||||||
|
rowUi.default ?: chars[0]
|
||||||
|
} else {
|
||||||
|
infoV
|
||||||
|
}
|
||||||
|
loginInfo[name] = char
|
||||||
|
val i = chars.indexOf(char)
|
||||||
|
selector.setSelectionSafely(i)
|
||||||
|
selector.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
|
var isInitializing = true
|
||||||
|
override fun onItemSelected(
|
||||||
|
parent: AdapterView<*>?,
|
||||||
|
view: View?,
|
||||||
|
position: Int,
|
||||||
|
id: Long
|
||||||
|
) {
|
||||||
|
if (isInitializing) { //忽略初始化选择
|
||||||
|
isInitializing = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasChange = true
|
||||||
|
loginInfo[name] = chars[position]
|
||||||
|
if (action != null) {
|
||||||
|
execute {
|
||||||
|
handleButtonClick(source, action, name, rowUis, false)
|
||||||
|
}.onError { e ->
|
||||||
|
AppLog.put("LoginUI Select $name JavaScript error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.flexbox.addView(it.root)
|
||||||
|
rowUi.style().apply {
|
||||||
|
when (this.layout_justifySelf) {
|
||||||
|
"flex_start" -> selector.gravity = Gravity.START
|
||||||
|
"flex_end" -> selector.gravity = Gravity.END
|
||||||
|
}
|
||||||
|
apply(it.root)
|
||||||
|
}
|
||||||
|
it.root.id = index + 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
Type.button -> ItemFilletTextBinding.inflate(
|
||||||
layoutInflater,
|
layoutInflater,
|
||||||
binding.root,
|
binding.root,
|
||||||
false
|
false
|
||||||
).let {
|
).let {
|
||||||
binding.flexbox.addView(it.root)
|
binding.flexbox.addView(it.root)
|
||||||
rowUi.style().apply(it.root)
|
rowUi.style().apply {
|
||||||
|
when (this.layout_justifySelf) {
|
||||||
|
"flex_start" -> it.textView.gravity = Gravity.START
|
||||||
|
"flex_end" -> it.textView.gravity = Gravity.END
|
||||||
|
}
|
||||||
|
apply(it.root)
|
||||||
|
}
|
||||||
it.root.id = index + 1000
|
it.root.id = index + 1000
|
||||||
it.textView.text = rowUi.name
|
it.textView.text = viewName ?: name
|
||||||
it.textView.setPadding(16.dpToPx())
|
it.textView.setPadding(16.dpToPx())
|
||||||
it.root.onClick {
|
|
||||||
handleButtonClick(source, rowUi, loginUi)
|
it.root.setOnClickListener {
|
||||||
|
val upTime = System.currentTimeMillis()
|
||||||
|
if (upTime - lastClickTime < 200) return@setOnClickListener
|
||||||
|
lastClickTime = upTime
|
||||||
|
handleButtonClick(source, action, name, rowUis, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.root.setOnLongClickListener {
|
||||||
|
handleButtonClick(source, action, name, rowUis, true)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Type.toggle -> ItemFilletTextBinding.inflate(
|
||||||
|
layoutInflater,
|
||||||
|
binding.root,
|
||||||
|
false
|
||||||
|
).let { itBinding ->
|
||||||
|
var newName = name
|
||||||
|
var left = true
|
||||||
|
binding.flexbox.addView(itBinding.root)
|
||||||
|
|
||||||
|
rowUi.style().apply {
|
||||||
|
when (this.layout_justifySelf) {
|
||||||
|
"flex_start" -> itBinding.textView.gravity = Gravity.START
|
||||||
|
"flex_end" -> itBinding.textView.gravity = Gravity.END
|
||||||
|
"right" -> left = false
|
||||||
|
}
|
||||||
|
apply(itBinding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
itBinding.root.id = index + 1000
|
||||||
|
val chars = rowUi.chars?.filterNotNull() ?: listOf("chars is null")
|
||||||
|
val infoV = loginInfo[name]
|
||||||
|
var char = if (infoV.isNullOrEmpty()) {
|
||||||
|
hasChange = true
|
||||||
|
rowUi.default ?: chars[0]
|
||||||
|
} else {
|
||||||
|
infoV
|
||||||
|
}
|
||||||
|
loginInfo[name] = char
|
||||||
|
|
||||||
|
val updateText = { currentChar: String, displayName: String ->
|
||||||
|
itBinding.textView.text =
|
||||||
|
if (left) currentChar + displayName else displayName + currentChar
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewName == null) {
|
||||||
|
updateText(char, name)
|
||||||
|
} else if (viewName.length in 3..19 && viewName.first() == '\'' && viewName.last() == '\'') {
|
||||||
|
val n = viewName.substring(1, viewName.length - 1)
|
||||||
|
rowUi.viewName = n
|
||||||
|
newName = n
|
||||||
|
updateText(char, n)
|
||||||
|
} else {
|
||||||
|
updateText(char, name)
|
||||||
|
execute {
|
||||||
|
evalUiJs(viewName)
|
||||||
|
}.onSuccess { n ->
|
||||||
|
if (!n.isNullOrEmpty()) {
|
||||||
|
rowUi.viewName = n
|
||||||
|
newName = n
|
||||||
|
updateText(char, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itBinding.textView.setPadding(16.dpToPx())
|
||||||
|
|
||||||
|
val toggleAction = { isLong: Boolean ->
|
||||||
|
val currentIndex = chars.indexOf(char)
|
||||||
|
val nextIndex = (currentIndex + 1) % chars.size
|
||||||
|
char = chars.getOrNull(nextIndex) ?: ""
|
||||||
|
hasChange = true
|
||||||
|
loginInfo[name] = char
|
||||||
|
updateText(char, newName)
|
||||||
|
handleButtonClick(source, action, name, rowUis, isLong)
|
||||||
|
}
|
||||||
|
|
||||||
|
itBinding.root.setOnClickListener {
|
||||||
|
val upTime = System.currentTimeMillis()
|
||||||
|
if (upTime - lastClickTime < 200) return@setOnClickListener
|
||||||
|
lastClickTime = upTime
|
||||||
|
toggleAction(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
itBinding.root.setOnLongClickListener {
|
||||||
|
toggleAction(true)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: NullPointerException) {
|
|
||||||
AppLog.put("登录UI JSON 数据错误", e, true)
|
|
||||||
}
|
}
|
||||||
binding.toolBar.inflateMenu(R.menu.source_login)
|
|
||||||
binding.toolBar.menu.applyTint(requireContext())
|
private fun buttonUi(source: BaseSource, rowUis: List<RowUi>?) {
|
||||||
|
rowUiBuilder(source, rowUis)
|
||||||
binding.toolBar.setOnMenuItemClickListener { item ->
|
binding.toolBar.setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.menu_ok -> {
|
R.id.menu_ok -> {
|
||||||
val loginData = getLoginData(loginUi)
|
oKToClose = true
|
||||||
|
val loginData = getLoginData(rowUis, true)
|
||||||
login(source, loginData)
|
login(source, loginData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,44 +609,86 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleButtonClick(source: BaseSource, rowUi: RowUi, loginUi: List<RowUi>) {
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val source = viewModel.source ?: return
|
||||||
|
val loginUiStr = source.loginUi ?: return
|
||||||
|
val codeStr = loginUiStr.let {
|
||||||
|
when {
|
||||||
|
it.startsWith("@js:") -> it.substring(4)
|
||||||
|
it.startsWith("<js>") -> it.substring(4, it.lastIndexOf("<"))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (codeStr != null) {
|
||||||
|
lifecycleScope.launch(Main) {
|
||||||
|
val loginUiJson = evalUiJs(codeStr)
|
||||||
|
rowUis = loginUi(loginUiJson)
|
||||||
|
buttonUi(source, rowUis)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowUis = loginUi(loginUiStr)
|
||||||
|
buttonUi(source, rowUis)
|
||||||
|
}
|
||||||
|
binding.toolBar.title = getString(R.string.login_source, source.getTag())
|
||||||
|
binding.toolBar.inflateMenu(R.menu.source_login)
|
||||||
|
binding.toolBar.menu.applyTint(requireContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleButtonClick(
|
||||||
|
source: BaseSource,
|
||||||
|
action: String?,
|
||||||
|
name: String,
|
||||||
|
rowUis: List<RowUi>,
|
||||||
|
isLongClick: Boolean
|
||||||
|
) {
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
if (rowUi.action.isAbsUrl()) {
|
if (action.isAbsUrl()) {
|
||||||
context?.openUrl(rowUi.action!!)
|
context?.openUrl(action!!)
|
||||||
} else if (rowUi.action != null) {
|
} else if (action != null) {
|
||||||
// JavaScript
|
// JavaScript
|
||||||
val buttonFunctionJS = rowUi.action!!
|
val buttonFunctionJS = action
|
||||||
val loginJS = source.getLoginJs() ?: return@launch
|
val loginJS = source.getLoginJs() ?: return@launch
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
runScriptWithContext {
|
runScriptWithContext {
|
||||||
source.evalJS("$loginJS\n$buttonFunctionJS") {
|
source.evalJS("$loginJS\n$buttonFunctionJS") {
|
||||||
put("result", getLoginData(loginUi))
|
put("java", sourceLoginJsExtensions)
|
||||||
|
put("result", getLoginData(rowUis))
|
||||||
|
put("book", viewModel.book)
|
||||||
|
put("chapter", viewModel.chapter)
|
||||||
|
put("isLongClick", isLongClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { e ->
|
}.onFailure { e ->
|
||||||
ensureActive()
|
ensureActive()
|
||||||
AppLog.put("LoginUI Button ${rowUi.name} JavaScript error", e)
|
AppLog.put("LoginUI Button $name JavaScript error", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getLoginData(loginUi: List<RowUi>?): HashMap<String, String> {
|
private fun getLoginData(
|
||||||
|
rowUis: List<RowUi>?,
|
||||||
|
save: Boolean = false
|
||||||
|
): MutableMap<String, String> {
|
||||||
val loginData = hashMapOf<String, String>()
|
val loginData = hashMapOf<String, String>()
|
||||||
loginUi?.forEachIndexed { index, rowUi ->
|
rowUis?.forEachIndexed { index, rowUi ->
|
||||||
when (rowUi.type) {
|
when (rowUi.type) {
|
||||||
"text", "password" -> {
|
Type.text, Type.password -> {
|
||||||
val rowView = binding.root.findViewById<View>(index + 1000)
|
val rowView = binding.root.findViewById<View>(index + 1000)
|
||||||
ItemSourceEditBinding.bind(rowView).editText.text?.let {
|
ItemSourceEditBinding.bind(rowView).editText.text.let {
|
||||||
loginData[rowUi.name] = it.toString()
|
loginData[rowUi.name] =
|
||||||
|
it?.toString() ?: rowUi.default ?: "" //没文本的时候存空字符串,而不是删除loginInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return loginData
|
if (save) {
|
||||||
|
return viewModel.loginInfo.apply { putAll(loginData) }
|
||||||
|
}
|
||||||
|
return viewModel.loginInfo.toMutableMap().apply { putAll(loginData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun login(source: BaseSource, loginData: HashMap<String, String>) {
|
private fun login(source: BaseSource, loginData: MutableMap<String, String>) {
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
if (loginData.isEmpty()) {
|
if (loginData.isEmpty()) {
|
||||||
source.removeLoginInfo()
|
source.removeLoginInfo()
|
||||||
@@ -184,6 +714,14 @@ class SourceLoginDialog : BaseBottomSheetDialogFragment(R.layout.dialog_login) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
|
if (!oKToClose && hasChange) {
|
||||||
|
val loginInfo = viewModel.loginInfo
|
||||||
|
if (loginInfo.isEmpty()) {
|
||||||
|
viewModel.source?.removeLoginInfo()
|
||||||
|
} else {
|
||||||
|
viewModel.source?.putLoginInfo(GSON.toJson(loginInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package io.legado.app.ui.login
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.data.entities.BaseSource
|
||||||
|
import io.legado.app.data.entities.HttpTTS
|
||||||
|
import io.legado.app.model.ReadAloud
|
||||||
|
import io.legado.app.ui.rss.read.RssJsExtensions
|
||||||
|
import io.legado.app.ui.widget.dialog.BottomWebViewDialog
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.sendToClip
|
||||||
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class SourceLoginJsExtensions(
|
||||||
|
activity: AppCompatActivity?, source: BaseSource?,
|
||||||
|
private val bookType: Int = 0,
|
||||||
|
private val callback: Callback? = null
|
||||||
|
) : RssJsExtensions(activity, source) {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun upUiData(data: Map<String, String?>?)
|
||||||
|
fun reUiView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upLoginData(data: Map<String, String?>?) {
|
||||||
|
callback?.upUiData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reLoginView() {
|
||||||
|
callback?.reUiView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshExplore() {
|
||||||
|
callback?.reUiView()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshBookInfo() {
|
||||||
|
postEvent(EventBus.REFRESH_BOOK_INFO, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyText(text: String) {
|
||||||
|
activityRef.get()?.sendToClip(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearTtsCache() {
|
||||||
|
if (getSource() !is HttpTTS) return
|
||||||
|
val activity = activityRef.get() ?: return
|
||||||
|
activity.lifecycleScope.launch(IO) {
|
||||||
|
ReadAloud.upReadAloudClass()
|
||||||
|
val ttsFolderPath =
|
||||||
|
"${activity.cacheDir.absolutePath}${File.separator}httpTTS${File.separator}"
|
||||||
|
FileUtils.listDirsAndFiles(ttsFolderPath)?.forEach {
|
||||||
|
FileUtils.delete(it.absolutePath)
|
||||||
|
}
|
||||||
|
activity.toastOnUi(R.string.clear_cache_success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun showBrowser(url: String, html: String, preloadJs: String? = null) {
|
||||||
|
val activity = activityRef.get() ?: return
|
||||||
|
val source = getSource() ?: return
|
||||||
|
activity.showDialogFragment(
|
||||||
|
BottomWebViewDialog(
|
||||||
|
source.getKey(),
|
||||||
|
bookType,
|
||||||
|
url,
|
||||||
|
html,
|
||||||
|
preloadJs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -5,28 +5,63 @@ import android.content.Intent
|
|||||||
import com.script.rhino.runScriptWithContext
|
import com.script.rhino.runScriptWithContext
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.BaseSource
|
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.NoStackTraceException
|
||||||
|
import io.legado.app.model.AudioPlay
|
||||||
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
|
||||||
class SourceLoginViewModel(application: Application) : BaseViewModel(application) {
|
class SourceLoginViewModel(application: Application) : BaseViewModel(application) {
|
||||||
|
|
||||||
var source: BaseSource? = null
|
var source: BaseSource? = null
|
||||||
var headerMap: Map<String, String> = emptyMap()
|
var headerMap: Map<String, String> = emptyMap()
|
||||||
|
var book: Book? = null
|
||||||
|
var bookType: Int = 0
|
||||||
|
var chapter: BookChapter? = null
|
||||||
|
var loginInfo: MutableMap<String, String> = mutableMapOf()
|
||||||
|
|
||||||
fun initData(intent: Intent, success: (bookSource: BaseSource) -> Unit, error: () -> Unit) {
|
fun initData(intent: Intent, success: (bookSource: BaseSource) -> Unit, error: () -> Unit) {
|
||||||
execute {
|
execute {
|
||||||
|
bookType = intent.getIntExtra("bookType", 0)
|
||||||
|
when (bookType) {
|
||||||
|
BookType.text -> {
|
||||||
|
source = ReadBook.bookSource
|
||||||
|
book = ReadBook.book?.also {
|
||||||
|
chapter =
|
||||||
|
appDb.bookChapterDao.getChapter(it.bookUrl, ReadBook.durChapterIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookType.audio -> {
|
||||||
|
source = AudioPlay.bookSource
|
||||||
|
book = AudioPlay.book
|
||||||
|
chapter = AudioPlay.durChapter
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
val sourceKey = intent.getStringExtra("key")
|
val sourceKey = intent.getStringExtra("key")
|
||||||
?: throw NoStackTraceException("没有参数")
|
?: throw NoStackTraceException("没有参数")
|
||||||
when (intent.getStringExtra("type")) {
|
val type = intent.getStringExtra("type")
|
||||||
"bookSource" -> source = appDb.bookSourceDao.getBookSource(sourceKey)
|
source = when (type) {
|
||||||
"rssSource" -> source = appDb.rssSourceDao.getByKey(sourceKey)
|
"bookSource" -> appDb.bookSourceDao.getBookSource(sourceKey)
|
||||||
"httpTts" -> source = appDb.httpTTSDao.get(sourceKey.toLong())
|
"rssSource" -> appDb.rssSourceDao.getByKey(sourceKey)
|
||||||
|
"httpTts" -> appDb.httpTTSDao.get(sourceKey.toLong())
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
val bookUrl = intent.getStringExtra("bookUrl")
|
||||||
|
book = bookUrl?.let {
|
||||||
|
appDb.bookDao.getBook(it) ?: appDb.searchBookDao.getSearchBook(it)?.toBook()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
headerMap = runScriptWithContext {
|
headerMap = runScriptWithContext {
|
||||||
source?.getHeaderMap(true) ?: emptyMap()
|
source?.getHeaderMap(true) ?: emptyMap()
|
||||||
}
|
}
|
||||||
|
source?.let { loginInfo = it.getLoginInfoMap() }
|
||||||
source
|
source
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ fun ReplaceRuleScreen(
|
|||||||
|
|
||||||
val uploadRepository: UploadRepository = koinInject()
|
val uploadRepository: UploadRepository = koinInject()
|
||||||
|
|
||||||
//TODO: 期望换为Navigation
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import io.legado.app.constant.PreferKey
|
|||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
import io.legado.app.data.repository.ReplaceRuleRepository
|
import io.legado.app.data.repository.ReplaceRuleRepository
|
||||||
import io.legado.app.exception.NoStackTraceException
|
|
||||||
import io.legado.app.help.ReplaceAnalyzer
|
import io.legado.app.help.ReplaceAnalyzer
|
||||||
import io.legado.app.help.http.decompressed
|
import io.legado.app.help.http.decompressed
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val firstArticle = articles.first()
|
val firstArticle = articles.first()
|
||||||
val dbFirstArticle = appDb.rssArticleDao.get(firstArticle.origin, firstArticle.link)
|
val dbFirstArticle = appDb.rssArticleDao.getByLink(firstArticle.origin, firstArticle.link)
|
||||||
val lastArticle = articles.last()
|
val lastArticle = articles.last()
|
||||||
val dbLastArticle = appDb.rssArticleDao.get(lastArticle.origin, lastArticle.link)
|
val dbLastArticle = appDb.rssArticleDao.getByLink(lastArticle.origin, lastArticle.link)
|
||||||
if (dbFirstArticle != null && dbLastArticle != null) {
|
if (dbFirstArticle != null && dbLastArticle != null) {
|
||||||
loadFinallyLiveData.postValue(false)
|
loadFinallyLiveData.postValue(false)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package io.legado.app.ui.rss.article
|
package io.legado.app.ui.rss.article
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -23,8 +24,14 @@ import io.legado.app.ui.login.SourceLoginActivity
|
|||||||
import io.legado.app.ui.rss.read.ReadRssActivity.RedirectPolicy
|
import io.legado.app.ui.rss.read.ReadRssActivity.RedirectPolicy
|
||||||
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
||||||
import io.legado.app.ui.widget.dialog.VariableDialog
|
import io.legado.app.ui.widget.dialog.VariableDialog
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.StartActivityContract
|
||||||
|
import io.legado.app.utils.gone
|
||||||
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
|
import io.legado.app.utils.themeColor
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
import io.legado.app.utils.visible
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -245,4 +252,14 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
|
|||||||
return RssArticlesFragment(sort.first, sort.second)
|
return RssArticlesFragment(sort.first, sort.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun start(context: Context, sortUrl: String?, sourceUrl: String, key: String? = null) {
|
||||||
|
context.startActivity<RssSortActivity> {
|
||||||
|
putExtra("sortUrl", sortUrl)
|
||||||
|
putExtra("sourceUrl", sourceUrl)
|
||||||
|
putExtra("key", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.rss.read
|
package io.legado.app.ui.rss.read
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -17,7 +18,6 @@ import android.webkit.SslErrorHandler
|
|||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import android.webkit.WebChromeClient
|
import android.webkit.WebChromeClient
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebResourceResponse
|
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
@@ -55,7 +55,6 @@ import io.legado.app.utils.setDarkeningAllowed
|
|||||||
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
||||||
import io.legado.app.utils.share
|
import io.legado.app.utils.share
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
import io.legado.app.utils.splitNotBlank
|
|
||||||
import io.legado.app.utils.startActivity
|
import io.legado.app.utils.startActivity
|
||||||
import io.legado.app.utils.textArray
|
import io.legado.app.utils.textArray
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
@@ -66,9 +65,7 @@ import kotlinx.coroutines.launch
|
|||||||
import org.apache.commons.text.StringEscapeUtils
|
import org.apache.commons.text.StringEscapeUtils
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import splitties.views.bottomPadding
|
import splitties.views.bottomPadding
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.util.regex.PatternSyntaxException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* rss阅读界面
|
* rss阅读界面
|
||||||
@@ -102,7 +99,7 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
|
|||||||
private var redirectPolicyMenu: MenuItem? = null
|
private var redirectPolicyMenu: MenuItem? = null
|
||||||
private var isFullScreen = false
|
private var isFullScreen = false
|
||||||
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
|
private var customWebViewCallback: WebChromeClient.CustomViewCallback? = null
|
||||||
private val rssJsExtensions by lazy { RssJsExtensions(this) }
|
private val rssJsExtensions by lazy { RssJsExtensions(this, viewModel.rssSource) }
|
||||||
|
|
||||||
fun getSource(): RssSource? {
|
fun getSource(): RssSource? {
|
||||||
return viewModel.rssSource
|
return viewModel.rssSource
|
||||||
@@ -615,4 +612,16 @@ class ReadRssActivity : VMBaseActivity<ActivityRssReadBinding, ReadRssViewModel>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun start(context: Context, title: String?, url: String, origin: String) {
|
||||||
|
context.startActivity<ReadRssActivity> {
|
||||||
|
putExtra("title", title ?: "")
|
||||||
|
putExtra("origin", origin)
|
||||||
|
putExtra("openUrl", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val webCookieManager by lazy { android.webkit.CookieManager.getInstance() }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
if (link != null) {
|
if (link != null) {
|
||||||
rssStar = appDb.rssStarDao.get(origin, link)
|
rssStar = appDb.rssStarDao.get(origin, link)
|
||||||
rssArticle = rssStar?.toRssArticle() ?: appDb.rssArticleDao.get(origin, link)
|
rssArticle = rssStar?.toRssArticle() ?: appDb.rssArticleDao.getByLink(origin, link)
|
||||||
val rssArticle = rssArticle ?: return@execute
|
val rssArticle = rssArticle ?: return@execute
|
||||||
if (!rssArticle.description.isNullOrBlank()) {
|
if (!rssArticle.description.isNullOrBlank()) {
|
||||||
contentLiveData.postValue(rssArticle.description!!)
|
contentLiveData.postValue(rssArticle.description!!)
|
||||||
|
|||||||
@@ -1,24 +1,178 @@
|
|||||||
package io.legado.app.ui.rss.read
|
package io.legado.app.ui.rss.read
|
||||||
|
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.data.entities.RssReadRecord
|
||||||
|
import io.legado.app.data.entities.RssSource
|
||||||
import io.legado.app.help.JsExtensions
|
import io.legado.app.help.JsExtensions
|
||||||
import io.legado.app.ui.association.AddToBookshelfDialog
|
import io.legado.app.ui.association.AddToBookshelfDialog
|
||||||
|
import io.legado.app.ui.book.explore.ExploreShowActivity
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
import io.legado.app.ui.book.search.SearchActivity
|
||||||
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
|
import io.legado.app.ui.rss.article.RssSortActivity
|
||||||
|
import io.legado.app.utils.isJsonObject
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class RssJsExtensions(private val activity: ReadRssActivity) : JsExtensions {
|
open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) : JsExtensions {
|
||||||
|
|
||||||
|
val activityRef: WeakReference<AppCompatActivity> = WeakReference(activity)
|
||||||
|
val sourceRef: WeakReference<BaseSource?> = WeakReference(source)
|
||||||
|
|
||||||
override fun getSource(): BaseSource? {
|
override fun getSource(): BaseSource? {
|
||||||
return activity.getSource()
|
return sourceRef.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTag(): String? {
|
||||||
|
return sourceRef.get()?.getTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun put(key: String, value: String): String {
|
||||||
|
getSource()?.put(key, value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun get(key: String): String {
|
||||||
|
return getSource()?.get(key) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun searchBook(key: String) {
|
fun searchBook(key: String) {
|
||||||
SearchActivity.start(activity, key)
|
searchBook(key, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun searchBook(key: String, searchScope: String?) {
|
||||||
|
activityRef.get()?.let {
|
||||||
|
SearchActivity.start(it, key, searchScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
fun addBook(bookUrl: String) {
|
fun addBook(bookUrl: String) {
|
||||||
activity.showDialogFragment(AddToBookshelfDialog(bookUrl))
|
activityRef.get()?.showDialogFragment(AddToBookshelfDialog(bookUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
@JvmOverloads
|
||||||
|
fun open(name: String, url: String? = null, title: String? = null, origin: String? = null) {
|
||||||
|
val activity = activityRef.get() ?: return
|
||||||
|
activity.lifecycleScope.launch(IO) {
|
||||||
|
val source = getSource() ?: return@launch
|
||||||
|
when (name) {
|
||||||
|
"login" -> {
|
||||||
|
if (activity is SourceLoginActivity) {
|
||||||
|
activity.toastOnUi("已在登录界面")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
val toSource = origin?.let { o ->
|
||||||
|
appDb.bookSourceDao.getBookSource(o)
|
||||||
|
} ?: source
|
||||||
|
if (toSource.loginUrl.isNullOrBlank()) {
|
||||||
|
activity.toastOnUi("源未配置登录")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
when (toSource) {
|
||||||
|
is BookSource -> {
|
||||||
|
withContext(Main) {
|
||||||
|
activity.startActivity<SourceLoginActivity> {
|
||||||
|
putExtra("type", "bookSource")
|
||||||
|
putExtra("key", toSource.bookSourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is RssSource -> {
|
||||||
|
withContext(Main) {
|
||||||
|
activity.startActivity<SourceLoginActivity> {
|
||||||
|
putExtra("type", "rssSource")
|
||||||
|
putExtra("key", toSource.sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"sort" -> {
|
||||||
|
val toSource = origin?.let { o ->
|
||||||
|
appDb.rssSourceDao.getByKey(o)
|
||||||
|
} ?: (source as? RssSource) ?: return@launch
|
||||||
|
val sortUrl = if (url.isJsonObject()) {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
title?.let {
|
||||||
|
JSONObject().put(title, url).toString()
|
||||||
|
} ?: url
|
||||||
|
}
|
||||||
|
val sourceUrl = toSource.sourceUrl
|
||||||
|
withContext(Main) {
|
||||||
|
RssSortActivity.start(activity, sortUrl, sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"rss" -> {
|
||||||
|
val toSource = origin?.let { o ->
|
||||||
|
appDb.rssSourceDao.getByKey(o)
|
||||||
|
} ?: (source as? RssSource) ?: return@launch
|
||||||
|
val title = title ?: toSource.sourceName
|
||||||
|
val sourceUrl = toSource.sourceUrl
|
||||||
|
val link = url ?: return@launch
|
||||||
|
val rss = appDb.rssStarDao.get(sourceUrl, link)?.toRecord()
|
||||||
|
?: appDb.rssArticleDao.getByLink(sourceUrl, link)?.toRecord()
|
||||||
|
val rssReadRecord = rss ?: RssReadRecord(
|
||||||
|
record = link,
|
||||||
|
title = title,
|
||||||
|
origin = sourceUrl,
|
||||||
|
readTime = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
appDb.rssReadRecordDao.insertRecord(rssReadRecord) //留下历史记录
|
||||||
|
withContext(Main) {
|
||||||
|
ReadRssActivity.start(activity, title, url, sourceUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"search" -> {
|
||||||
|
title?.let {
|
||||||
|
val searchScope = origin?.let { o ->
|
||||||
|
appDb.bookSourceDao.getBookSource(o)?.let { s ->
|
||||||
|
"${s.bookSourceName.replace(":", "")}::${o}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Main) {
|
||||||
|
searchBook(it, searchScope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"explore" -> {
|
||||||
|
val toSource = origin?.let { o ->
|
||||||
|
appDb.bookSourceDao.getBookSource(o)
|
||||||
|
} ?: (source as? BookSource) ?: return@launch
|
||||||
|
val sourceUrl = toSource.bookSourceUrl
|
||||||
|
withContext(Main) {
|
||||||
|
activity.startActivity<ExploreShowActivity> {
|
||||||
|
putExtra("exploreName", title)
|
||||||
|
putExtra("sourceUrl", sourceUrl)
|
||||||
|
putExtra("exploreUrl", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -28,6 +28,7 @@ import androidx.compose.material3.AssistChip
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -44,7 +45,6 @@ import androidx.compose.material3.SnackbarHost
|
|||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -148,7 +148,7 @@ fun <T> BatchImportDialog(
|
|||||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
AnimatedText(
|
AnimatedText(
|
||||||
if (selectedCount > 0)
|
if (selectedCount > 0)
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
package io.legado.app.ui.widget.dialog
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.net.http.SslError
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.webkit.JavascriptInterface
|
||||||
|
import android.webkit.SslErrorHandler
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebChromeClient.CustomViewCallback
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.view.isNotEmpty
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseBottomSheetDialogFragment
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.BaseSource
|
||||||
|
import io.legado.app.databinding.DialogWebViewBinding
|
||||||
|
import io.legado.app.help.WebCacheManager
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.JS_INJECTION
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.basicJs
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameBasic
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameCache
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameJava
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions.Companion.nameSource
|
||||||
|
import io.legado.app.lib.theme.ThemeStore
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
|
import io.legado.app.ui.association.OnLineImportActivity
|
||||||
|
import io.legado.app.utils.get
|
||||||
|
import io.legado.app.utils.invisible
|
||||||
|
import io.legado.app.utils.longSnackbar
|
||||||
|
import io.legado.app.utils.openUrl
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
import io.legado.app.utils.visible
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class BottomWebViewDialog() :
|
||||||
|
BaseBottomSheetDialogFragment(R.layout.dialog_web_view) {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
sourceKey: String,
|
||||||
|
bookType: Int,
|
||||||
|
url: String,
|
||||||
|
html: String,
|
||||||
|
preloadJs: String? = null
|
||||||
|
) : this() {
|
||||||
|
arguments = Bundle().apply {
|
||||||
|
putString("sourceKey", sourceKey)
|
||||||
|
putInt("bookType", bookType)
|
||||||
|
putString("url", url)
|
||||||
|
putString("html", html)
|
||||||
|
putString("preloadJs", preloadJs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val binding by viewBinding(DialogWebViewBinding::bind)
|
||||||
|
|
||||||
|
private val bottomSheet by lazy {
|
||||||
|
dialog?.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val behavior by lazy {
|
||||||
|
bottomSheet?.let { BottomSheetBehavior.from(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var currentWebView: WebView
|
||||||
|
|
||||||
|
private var source: BaseSource? = null
|
||||||
|
private var isFullScreen = false
|
||||||
|
private var customWebViewCallback: CustomViewCallback? = null
|
||||||
|
private var originOrientation: Int? = null
|
||||||
|
private var needClearHistory = true
|
||||||
|
|
||||||
|
override fun show(manager: FragmentManager, tag: String?) {
|
||||||
|
runCatching {
|
||||||
|
manager.beginTransaction().remove(this).commit()
|
||||||
|
super.show(manager, tag)
|
||||||
|
}.onFailure {
|
||||||
|
AppLog.put("显示对话框失败 tag:$tag", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFragmentCreated(
|
||||||
|
view: View,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) {
|
||||||
|
if (!AppConfig.isEInkMode) {
|
||||||
|
view.setBackgroundColor(ThemeStore.backgroundColor())
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWebView = createWebView()
|
||||||
|
binding.webViewContainer.addView(currentWebView)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val args = arguments ?: run {
|
||||||
|
dismiss(); return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceKey = args.getString("sourceKey") ?: return@launch
|
||||||
|
val url = args.getString("url") ?: return@launch
|
||||||
|
var html = args.getString("html") ?: return@launch
|
||||||
|
|
||||||
|
args.getString("preloadJs")?.let { preloadJs ->
|
||||||
|
html = if (html.contains("<head>")) {
|
||||||
|
html.replaceFirst(
|
||||||
|
"<head>",
|
||||||
|
"<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script>"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"<head><script>(() => {$JS_INJECTION\n$preloadJs\n})();</script></head>$html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source = appDb.bookSourceDao.getBookSource(sourceKey) ?: run {
|
||||||
|
activity?.toastOnUi("no find bookSource")
|
||||||
|
dismiss()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val bookType = args.getInt("bookType", 0)
|
||||||
|
val analyzeUrl = AnalyzeUrl(url, source = source, coroutineContext = coroutineContext)
|
||||||
|
|
||||||
|
currentWebView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
||||||
|
behavior?.isDraggable = scrollY == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWebView.post {
|
||||||
|
initWebView(
|
||||||
|
analyzeUrl.url,
|
||||||
|
html,
|
||||||
|
analyzeUrl.headerMap,
|
||||||
|
bookType
|
||||||
|
)
|
||||||
|
currentWebView.clearHistory()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog?.setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||||
|
when {
|
||||||
|
binding.customWebView.isNotEmpty() -> {
|
||||||
|
customWebViewCallback?.onCustomViewHidden()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
currentWebView.canGoBack() -> {
|
||||||
|
currentWebView.goBack()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
dismiss()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createWebView(): WebView =
|
||||||
|
WebView(requireContext()).apply {
|
||||||
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initWebView(
|
||||||
|
url: String,
|
||||||
|
html: String,
|
||||||
|
headerMap: HashMap<String, String>,
|
||||||
|
bookType: Int
|
||||||
|
) {
|
||||||
|
currentWebView.apply {
|
||||||
|
webChromeClient = CustomWebChromeClient()
|
||||||
|
webViewClient = CustomWebViewClient()
|
||||||
|
|
||||||
|
settings.userAgentString =
|
||||||
|
headerMap.get(AppConst.UA_NAME, true)
|
||||||
|
|
||||||
|
addJavascriptInterface(JSInterface(this@BottomWebViewDialog), nameBasic)
|
||||||
|
|
||||||
|
source?.let { source ->
|
||||||
|
(activity as? AppCompatActivity)?.let { act ->
|
||||||
|
addJavascriptInterface(
|
||||||
|
WebJsExtensions(source, act, this, bookType),
|
||||||
|
nameJava
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addJavascriptInterface(source, nameSource)
|
||||||
|
addJavascriptInterface(WebCacheManager, nameCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
customWebViewCallback?.onCustomViewHidden()
|
||||||
|
|
||||||
|
currentWebView.apply {
|
||||||
|
stopLoading()
|
||||||
|
loadUrl("about:blank")
|
||||||
|
clearHistory()
|
||||||
|
removeAllViews()
|
||||||
|
|
||||||
|
webChromeClient = WebChromeClient()
|
||||||
|
webViewClient = WebViewClient()
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
originOrientation?.let {
|
||||||
|
activity?.requestedOrientation = it
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class JSInterface(dialog: BottomWebViewDialog) {
|
||||||
|
private val ref = WeakReference(dialog)
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun lockOrientation(orientation: String) {
|
||||||
|
val fra = ref.get() ?: return
|
||||||
|
val act = fra.activity ?: return
|
||||||
|
|
||||||
|
if (fra.isFullScreen && fra.dialog?.isShowing == true) {
|
||||||
|
act.runOnUiThread {
|
||||||
|
act.requestedOrientation = when (orientation) {
|
||||||
|
"portrait", "portrait-primary" ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
"portrait-secondary" ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||||
|
|
||||||
|
"landscape", "landscape-primary" ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
"landscape-secondary" ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||||
|
|
||||||
|
"any", "unspecified" ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
||||||
|
|
||||||
|
else ->
|
||||||
|
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun onCloseRequested() {
|
||||||
|
ref.get()?.activity?.runOnUiThread {
|
||||||
|
ref.get()?.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CustomWebChromeClient : WebChromeClient() {
|
||||||
|
|
||||||
|
override fun onShowCustomView(view: View?, callback: CustomViewCallback?) {
|
||||||
|
isFullScreen = true
|
||||||
|
binding.webViewContainer.invisible()
|
||||||
|
binding.customWebView.addView(view)
|
||||||
|
customWebViewCallback = callback
|
||||||
|
behavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
originOrientation = activity?.requestedOrientation
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHideCustomView() {
|
||||||
|
isFullScreen = false
|
||||||
|
binding.webViewContainer.visible()
|
||||||
|
binding.customWebView.removeAllViews()
|
||||||
|
customWebViewCallback = null
|
||||||
|
originOrientation?.let {
|
||||||
|
activity?.requestedOrientation = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCloseWindow(window: WebView?) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CustomWebViewClient : WebViewClient() {
|
||||||
|
|
||||||
|
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||||
|
if (needClearHistory) {
|
||||||
|
needClearHistory = false
|
||||||
|
currentWebView.clearHistory()
|
||||||
|
}
|
||||||
|
super.onPageStarted(view, url, favicon)
|
||||||
|
currentWebView.evaluateJavascript(basicJs, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?
|
||||||
|
): Boolean {
|
||||||
|
return request?.url?.let(::handleUri) ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
|
||||||
|
return url?.toUri()?.let(::handleUri) ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUri(uri: Uri): Boolean =
|
||||||
|
when (uri.scheme) {
|
||||||
|
"http", "https" -> false
|
||||||
|
"legado", "yuedu" -> {
|
||||||
|
startActivity<OnLineImportActivity> { data = uri }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
binding.root.longSnackbar(
|
||||||
|
R.string.jump_to_another_app,
|
||||||
|
R.string.confirm
|
||||||
|
) {
|
||||||
|
activity?.openUrl(uri)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("WebViewClientOnReceivedSslError")
|
||||||
|
override fun onReceivedSslError(
|
||||||
|
view: WebView?,
|
||||||
|
handler: SslErrorHandler?,
|
||||||
|
error: SslError?
|
||||||
|
) {
|
||||||
|
handler?.proceed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,15 +7,18 @@ import android.icu.text.Collator
|
|||||||
import android.icu.util.ULocale
|
import android.icu.util.ULocale
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
|
import androidx.core.net.toUri
|
||||||
import cn.hutool.core.net.URLEncodeUtil
|
import cn.hutool.core.net.URLEncodeUtil
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.AppPattern.dataUriRegex
|
import io.legado.app.constant.AppPattern.dataUriRegex
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.Character.codePointCount
|
import java.lang.Character.codePointCount
|
||||||
import java.lang.Character.offsetByCodePoints
|
import java.lang.Character.offsetByCodePoints
|
||||||
|
import java.net.InetAddress
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
|
||||||
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
|
fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim()
|
||||||
|
|
||||||
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
|
fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
|
||||||
@@ -23,7 +26,7 @@ fun String?.isContentScheme(): Boolean = this?.startsWith("content://") == true
|
|||||||
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
|
||||||
|
|
||||||
fun String.parseToUri(): Uri {
|
fun String.parseToUri(): Uri {
|
||||||
return if (isUri()) Uri.parse(this) else {
|
return if (isUri()) this.toUri() else {
|
||||||
Uri.fromFile(File(this))
|
Uri.fromFile(File(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +78,7 @@ fun String?.isTrue(nullIsTrue: Boolean = false): Boolean {
|
|||||||
if (this.isNullOrBlank() || this == "null") {
|
if (this.isNullOrBlank() || this == "null") {
|
||||||
return nullIsTrue
|
return nullIsTrue
|
||||||
}
|
}
|
||||||
return !this.trim().matches("(?i)^(false|no|not|0)$".toRegex())
|
return !this.trim().matches("(?i)^(?:false|no|not|0|0.0)$".toRegex())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.isHex(): Boolean {
|
fun String.isHex(): Boolean {
|
||||||
@@ -144,3 +147,41 @@ fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this)
|
|||||||
fun String.normalizeFileName(): String {
|
fun String.normalizeFileName(): String {
|
||||||
return replace(AppPattern.fileNameRegex2, "_")
|
return replace(AppPattern.fileNameRegex2, "_")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串加上转义,方便传递字符串到浏览器
|
||||||
|
*/
|
||||||
|
fun String.escapeForJs(): String {
|
||||||
|
return this.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"").replace("'", "\\'")
|
||||||
|
.replace("\n", "\\n").replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t")
|
||||||
|
.replace("\u2028", "\\u2028")
|
||||||
|
.replace("\u2029", "\\u2029")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将ip字符串转为InetAddress
|
||||||
|
*/
|
||||||
|
fun String.parseIpsFromString(): List<InetAddress>? =
|
||||||
|
split(",")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
.mapNotNull { it.runCatching { InetAddress.getByName(this) }.getOrNull() }
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
|
|
||||||
|
fun String.quoteReplacementJs(): String {
|
||||||
|
if (!this.contains('\\')) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
val sb = StringBuilder()
|
||||||
|
for (c in this) {
|
||||||
|
if (c == '\\') {
|
||||||
|
sb.append("\\\\")
|
||||||
|
} else {
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import android.widget.EdgeEffect
|
|||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
|
import android.widget.Spinner
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
@@ -330,3 +331,9 @@ fun View.setOnApplyWindowInsetsListenerCompat(listener: (View, WindowInsetsCompa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Spinner.setSelectionSafely(position: Int) {
|
||||||
|
val count = adapter?.count ?: 0
|
||||||
|
if (count > 0) {
|
||||||
|
setSelection(position.coerceIn(0, count - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/web_view_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/custom_web_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sp_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="3dp"
|
||||||
|
android:text="@string/book_type" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatSpinner
|
||||||
|
android:id="@+id/sp_type"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1382,4 +1382,5 @@
|
|||||||
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
|
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
|
||||||
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
||||||
<string name="show_swipe_animation">启用滑动时动画</string>
|
<string name="show_swipe_animation">启用滑动时动画</string>
|
||||||
|
<string name="click_image_way">点击图片行为</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -52,6 +52,13 @@
|
|||||||
<item>4</item>
|
<item>4</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="click_image_way_value">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>3</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="double_page_value">
|
<string-array name="double_page_value">
|
||||||
<item>0</item>
|
<item>0</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
|
|||||||
@@ -116,6 +116,13 @@
|
|||||||
<item>@string/screen_portrait_reversed</item>
|
<item>@string/screen_portrait_reversed</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="click_image_way_title">
|
||||||
|
<item>新包名阅读行为</item>
|
||||||
|
<item>预览图片</item>
|
||||||
|
<item>经典阅读行为</item>
|
||||||
|
<item>不响应</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="double_page_title">
|
<string-array name="double_page_title">
|
||||||
<item>Single page</item>
|
<item>Single page</item>
|
||||||
<item>Double page</item>
|
<item>Double page</item>
|
||||||
|
|||||||
@@ -1386,4 +1386,5 @@
|
|||||||
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
|
<string name="audio_cache_clean_time">音频缓存保留时间(分钟)</string>
|
||||||
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
<string name="audio_cache_clean_time_summary">当前%d, 输入 0 代表退出即刻清理。</string>
|
||||||
<string name="show_swipe_animation">启用滑动时动画</string>
|
<string name="show_swipe_animation">启用滑动时动画</string>
|
||||||
|
<string name="click_image_way">点击图片行为</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -196,10 +196,13 @@
|
|||||||
android:title="@string/no_anim_scroll_page"
|
android:title="@string/no_anim_scroll_page"
|
||||||
app:isBottomBackground="true" />
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
<io.legado.app.lib.prefs.SwitchPreference
|
<io.legado.app.lib.prefs.NameListPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="2"
|
||||||
android:key="previewImageByClick"
|
android:entries="@array/click_image_way_title"
|
||||||
android:title="@string/preview_image_by_click"
|
android:entryValues="@array/click_image_way_value"
|
||||||
|
android:key="clickImgWay"
|
||||||
|
android:title="@string/click_image_way"
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
app:isBottomBackground="true" />
|
app:isBottomBackground="true" />
|
||||||
|
|
||||||
<io.legado.app.lib.prefs.SwitchPreference
|
<io.legado.app.lib.prefs.SwitchPreference
|
||||||
|
|||||||
Reference in New Issue
Block a user