Merge branch 'master' into master

pull/1128/head
kunfei 3 years ago committed by GitHub
commit ee0a6e63bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 44
      app/src/main/java/io/legado/app/data/entities/BookChapter.kt
  2. 131
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt
  3. 14
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  4. 91
      app/src/main/java/io/legado/app/model/analyzeRule/RuleAnalyzer.kt
  5. 7
      app/src/main/java/io/legado/app/service/help/ReadBook.kt
  6. 2
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@ -15,30 +15,30 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@Entity( @Entity(
tableName = "chapters", tableName = "chapters",
primaryKeys = ["url", "bookUrl"], primaryKeys = ["url", "bookUrl"],
indices = [(Index(value = ["bookUrl"], unique = false)), indices = [(Index(value = ["bookUrl"], unique = false)),
(Index(value = ["bookUrl", "index"], unique = true))], (Index(value = ["bookUrl", "index"], unique = true))],
foreignKeys = [(ForeignKey( foreignKeys = [(ForeignKey(
entity = Book::class, entity = Book::class,
parentColumns = ["bookUrl"], parentColumns = ["bookUrl"],
childColumns = ["bookUrl"], childColumns = ["bookUrl"],
onDelete = ForeignKey.CASCADE onDelete = ForeignKey.CASCADE
))] ))]
) // 删除书籍时自动删除章节 ) // 删除书籍时自动删除章节
data class BookChapter( data class BookChapter(
var url: String = "", // 章节地址 var url: String = "", // 章节地址
var title: String = "", // 章节标题 var title: String = "", // 章节标题
var baseUrl: String = "", //用来拼接相对url var baseUrl: String = "", //用来拼接相对url
var bookUrl: String = "", // 书籍地址 var bookUrl: String = "", // 书籍地址
var index: Int = 0, // 章节序号 var index: Int = 0, // 章节序号
var resourceUrl: String? = null, // 音频真实URL var resourceUrl: String? = null, // 音频真实URL
var tag: String? = null, // var tag: String? = null, //
var start: Long? = null, // 章节起始位置 var start: Long? = null, // 章节起始位置
var end: Long? = null, // 章节终止位置 var end: Long? = null, // 章节终止位置
var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId
var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId
var variable: String? = null //变量 var variable: String? = null //变量
) : Parcelable { ) : Parcelable {
@delegate:Transient @delegate:Transient

@ -122,7 +122,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
@JvmOverloads @JvmOverloads
fun getStringList(rule: String?, isUrl: Boolean = false): List<String>? { fun getStringList(rule: String?, isUrl: Boolean = false): List<String>? {
if (rule.isNullOrEmpty()) return null if (rule.isNullOrEmpty()) return null
val ruleList = splitSourceRule(rule,true) val ruleList = splitSourceRule(rule, true)
return getStringList(ruleList, isUrl) return getStringList(ruleList, isUrl)
} }
@ -282,7 +282,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun getElements(ruleStr: String): List<Any> { fun getElements(ruleStr: String): List<Any> {
var result: Any? = null var result: Any? = null
val ruleList = splitSourceRule(ruleStr,true) val ruleList = splitSourceRule(ruleStr, true)
content?.let { o -> content?.let { o ->
if (ruleList.isNotEmpty()) result = o if (ruleList.isNotEmpty()) result = o
for (sourceRule in ruleList) { for (sourceRule in ruleList) {
@ -357,61 +357,34 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
/** /**
* 分解规则生成规则列表 * 分解规则生成规则列表
*/ */
fun splitSourceRule(ruleStr: String?,isList:Boolean = false): List<SourceRule> { fun splitSourceRule(ruleStr: String?, isList: Boolean = false): List<SourceRule> {
if (ruleStr.isNullOrEmpty()) return ArrayList<SourceRule>() if (ruleStr.isNullOrEmpty()) return ArrayList<SourceRule>()
val ruleList = ArrayList<SourceRule>() val ruleList = ArrayList<SourceRule>()
//检测Mode
var mMode: Mode = Mode.Default var mMode: Mode = Mode.Default
fun mode(ruleStr0:String)=when { var start = 0
ruleStr0.startsWith("@@") -> { //仅首字符为:时为AllInOne,其实:与伪类选择器冲突,建议改成?更合理
mMode = Mode.Default if (isList && ruleStr.startsWith(":")) {
ruleStr0.substring(2)
}
ruleStr0.startsWith("@XPath:", true) -> {
mMode = Mode.XPath
ruleStr0.substring(7)
}
ruleStr0.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头
mMode = Mode.XPath
ruleStr0
}
ruleStr0.startsWith("@Json:", true) -> {
mMode = Mode.Json
ruleStr0.substring(6)
}
( ruleStr0[1] == '.' || ruleStr0[1] == '[') && ruleStr0[0] == '$' || isJSON -> {
mMode = Mode.Json
ruleStr0
}
else -> {
mMode = Mode.Default
ruleStr0
}
}
//拆分为规则列表
var start = if(isList && ruleStr.startsWith(":")){ //仅首字符为:时为AllInOne,其实:与伪类选择器冲突,建议改成?更合理
mMode = Mode.Regex mMode = Mode.Regex
isRegex = true isRegex = true
1 start = 1
}else 0 }
var tmp: String var tmp: String
val jsMatcher = JS_PATTERN.matcher(ruleStr) val jsMatcher = JS_PATTERN.matcher(ruleStr)
while (jsMatcher.find()){ while (jsMatcher.find()) {
if (jsMatcher.start() > start) { if (jsMatcher.start() > start) {
tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' } tmp = ruleStr.substring(start, jsMatcher.start()).trim { it <= ' ' }
if (tmp.isNotEmpty()) { if (tmp.isNotEmpty()) {
ruleList.add(SourceRule(mode(tmp), mMode)) ruleList.add(SourceRule(tmp, mMode))
} }
} }
ruleList.add(SourceRule(jsMatcher.group(2)?:jsMatcher.group(1), Mode.Js)) ruleList.add(SourceRule(jsMatcher.group(2) ?: jsMatcher.group(1), Mode.Js))
start = jsMatcher.end() start = jsMatcher.end()
} }
if (ruleStr.length > start){ if (ruleStr.length > start) {
tmp = ruleStr.substring(start).trim { it <= ' ' } tmp = ruleStr.substring(start).trim { it <= ' ' }
if (tmp.isNotEmpty()) { if (tmp.isNotEmpty()) {
ruleList.add(SourceRule(mode(tmp), mMode)) ruleList.add(SourceRule(tmp, mMode))
} }
} }
@ -437,43 +410,56 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
init { init {
this.mode = mainMode this.mode = mainMode
rule = when {
ruleStr.startsWith("@CSS:", true) -> {
mode = Mode.Default
ruleStr
}
ruleStr.startsWith("@@") -> {
mode = Mode.Default
ruleStr.substring(2)
}
ruleStr.startsWith("@XPath:", true) -> {
mode = Mode.XPath
ruleStr.substring(7)
}
ruleStr.startsWith("/") -> {//XPath特征很明显,无需配置单独的识别标头
mode = Mode.XPath
ruleStr
}
ruleStr.startsWith("@Json:", true) -> {
mode = Mode.Json
ruleStr.substring(6)
}
(ruleStr[1] == '.' || ruleStr[1] == '[') && ruleStr.startsWith("$.") || isJSON -> {
mode = Mode.Json
ruleStr
}
else -> {
ruleStr
}
}
//分离put //分离put
rule = splitPutRule(ruleStr, putMap) rule = splitPutRule(rule, putMap)
//@get,{{ }}, 拆分 //@get,{{ }}, 拆分
var start = 0 var start = 0
var tmp: String var tmp: String
val evalMatcher = evalPattern.matcher(rule) val evalMatcher = evalPattern.matcher(rule)
if(evalMatcher.find()){ if (evalMatcher.find()) {
if (mode != Mode.Js && mode != Mode.Regex
var modeX = mode == Mode.Js || mode == Mode.Regex && evalMatcher.start() == 0
if (evalMatcher.start() > 0 ) { ) {
tmp = rule.substring(0, evalMatcher.start()) mode = Mode.Regex
modeX = modeX || tmp.contains("##")
splitRegex(tmp)
}
if(!modeX)mode = Mode.Regex
tmp = evalMatcher.group()
when {
tmp.startsWith("@get:", true) -> {
ruleType.add(getRuleType)
ruleParam.add(tmp.substring(6, tmp.lastIndex))
}
tmp.startsWith("{{") -> {
ruleType.add(jsRuleType)
ruleParam.add(tmp.substring(2, tmp.length - 2))
}
else -> {
splitRegex(tmp)
}
} }
do {
start = evalMatcher.end()
while (evalMatcher.find()){
if (evalMatcher.start() > start) { if (evalMatcher.start() > start) {
tmp = rule.substring(start, evalMatcher.start()) tmp = rule.substring(start, evalMatcher.start())
if (mode != Mode.Js && mode != Mode.Regex
&& start == 0 && !tmp.contains("##")
) {
mode = Mode.Regex
}
splitRegex(tmp) splitRegex(tmp)
} }
tmp = evalMatcher.group() tmp = evalMatcher.group()
@ -491,9 +477,8 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
} }
} }
start = evalMatcher.end() start = evalMatcher.end()
} } while (evalMatcher.find())
} }
if (rule.length > start) { if (rule.length > start) {
tmp = rule.substring(start) tmp = rule.substring(start)
splitRegex(tmp) splitRegex(tmp)
@ -509,11 +494,11 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
val ruleStrArray = ruleStr.split("##") val ruleStrArray = ruleStr.split("##")
val regexMatcher = regexPattern.matcher(ruleStrArray[0]) val regexMatcher = regexPattern.matcher(ruleStrArray[0])
if(regexMatcher.find()) { if (regexMatcher.find()) {
if (mode != Mode.Js && mode != Mode.Regex) { if (mode != Mode.Js && mode != Mode.Regex) {
mode = Mode.Regex mode = Mode.Regex
} }
do{ do {
if (regexMatcher.start() > start) { if (regexMatcher.start() > start) {
tmp = ruleStr.substring(start, regexMatcher.start()) tmp = ruleStr.substring(start, regexMatcher.start())
ruleType.add(defaultRuleType) ruleType.add(defaultRuleType)
@ -523,7 +508,7 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions {
ruleType.add(tmp.substring(1).toInt()) ruleType.add(tmp.substring(1).toInt())
ruleParam.add(tmp) ruleParam.add(tmp)
start = regexMatcher.end() start = regexMatcher.end()
}while (regexMatcher.find()) } while (regexMatcher.find())
} }
if (ruleStr.length > start) { if (ruleStr.length > start) {
tmp = ruleStr.substring(start) tmp = ruleStr.substring(start)

@ -84,7 +84,7 @@ class AnalyzeUrl(
ruleUrl = tmp.replace("@result", ruleUrl) ruleUrl = tmp.replace("@result", ruleUrl)
} }
} }
ruleUrl = evalJS(jsMatcher.group(2)?:jsMatcher.group(1), ruleUrl) as String ruleUrl = evalJS(jsMatcher.group(2) ?: jsMatcher.group(1), ruleUrl) as String
start = jsMatcher.end() start = jsMatcher.end()
} }
if (ruleUrl.length > start) { if (ruleUrl.length > start) {
@ -116,14 +116,14 @@ class AnalyzeUrl(
bindings["book"] = book bindings["book"] = book
//替换所有内嵌{{js}} //替换所有内嵌{{js}}
val url = analyze.innerRule("{{","}}"){ val url = analyze.innerRule("{{", "}}") {
when(val jsEval = SCRIPT_ENGINE.eval(it, bindings)){ when (val jsEval = SCRIPT_ENGINE.eval(it, bindings)) {
is String -> jsEval is String -> jsEval
jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval) jsEval is Double && jsEval % 1.0 == 0.0 -> String.format("%.0f", jsEval)
else -> jsEval.toString() else -> jsEval.toString()
} }
} }
if(url.isNotEmpty())ruleUrl = url if (url.isNotEmpty()) ruleUrl = url
} }
//page //page
page?.let { page?.let {
@ -143,7 +143,6 @@ class AnalyzeUrl(
* 处理URL * 处理URL
*/ */
private fun initUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。 private fun initUrl() { //replaceKeyPageJs已经替换掉额外内容,此处url是基础形式,可以直接切首个‘,’之前字符串。
val urlMatcher = paramPattern.matcher(ruleUrl) val urlMatcher = paramPattern.matcher(ruleUrl)
urlHasQuery = if(urlMatcher.find())ruleUrl.substring(0,urlMatcher.start()) else ruleUrl urlHasQuery = if(urlMatcher.find())ruleUrl.substring(0,urlMatcher.start()) else ruleUrl
url = NetworkUtils.getAbsoluteURL(baseUrl,urlHasQuery ) url = NetworkUtils.getAbsoluteURL(baseUrl,urlHasQuery )
@ -152,7 +151,6 @@ class AnalyzeUrl(
} }
if(urlHasQuery.length != ruleUrl.length ) { if(urlHasQuery.length != ruleUrl.length ) {
GSON.fromJsonObject<UrlOption>(ruleUrl.substring(urlMatcher.end()))?.let { option -> GSON.fromJsonObject<UrlOption>(ruleUrl.substring(urlMatcher.end()))?.let { option ->
option.method?.let { option.method?.let {
if (it.equals("POST", true)) method = RequestMethod.POST if (it.equals("POST", true)) method = RequestMethod.POST
} }
@ -190,9 +188,9 @@ class AnalyzeUrl(
RequestMethod.GET -> { RequestMethod.GET -> {
if (!useWebView) { if (!useWebView) {
val pos = url.indexOf('?') val pos = url.indexOf('?')
if(pos != -1) { if (pos != -1) {
analyzeFields(url.substring(pos + 1)) analyzeFields(url.substring(pos + 1))
url = url.substring(0,pos) url = url.substring(0, pos)
} }
} }
} }

@ -1,8 +1,9 @@
package io.legado.app.model.analyzeRule package io.legado.app.model.analyzeRule
import io.legado.app.utils.isJson import io.legado.app.utils.isJson
import java.util.ArrayList import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.collections.HashMap
//通用的规则切分处理 //通用的规则切分处理
@ -70,6 +71,7 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
} }
return false return false
} }
/** /**
* 从剩余字串中拉出一个字符串直到但不包括匹配序列匹配参数列表中一项即为匹配或剩余字串用完 * 从剩余字串中拉出一个字符串直到但不包括匹配序列匹配参数列表中一项即为匹配或剩余字串用完
* @param seq 匹配字符串序列 * @param seq 匹配字符串序列
@ -369,8 +371,8 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
* *
* */ * */
fun innerRule( fun innerRule(
startStr:String, startStr: String,
endStr:String, endStr: String,
fr: (String) -> String? fr: (String) -> String?
): String { ): String {
@ -380,13 +382,18 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
val posPre = pos //记录consumeTo匹配位置 val posPre = pos //记录consumeTo匹配位置
if (consumeTo(endStr)) { if (consumeTo(endStr)) {
val frv = fr(queue.substring(posPre, pos)) val frv = fr(queue.substring(posPre, pos))
st.append(queue.substring(startX, posPre - startStr.length) + frv) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串 st.append(
pos += endStr.length //跳过结束字符串 queue.substring(
startX = pos //记录下次规则起点 startX,
posPre - startStr.length
) + frv
) //压入内嵌规则前的内容,及内嵌规则解析得到的字符串
pos += endStr.length //跳过结束字符串
startX = pos //记录下次规则起点
} }
} }
return if(startX == 0) queue else st.apply { return if (startX == 0) queue else st.apply {
append(queue.substring(startX)) append(queue.substring(startX))
}.toString() }.toString()
} }
@ -394,8 +401,10 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
//-----------此处向下的函数和变量都未被使用,但以后要用-------- //-----------此处向下的函数和变量都未被使用,但以后要用--------
val ruleTypeList = ArrayList<String>() val ruleTypeList = ArrayList<String>()
//设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced //设置平衡组函数,json或JavaScript时设置成chompCodeBalanced,否则为chompRuleBalanced
val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced val chompBalanced = if (code) ::chompCodeBalanced else ::chompRuleBalanced
enum class Mode { enum class Mode {
XPath, Json, Default, Js, Regex XPath, Json, Default, Js, Regex
} }
@ -450,11 +459,11 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
pos = st //位置回到筛选器处 pos = st //位置回到筛选器处
val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符 val next = if (queue[pos] == '[') ']' else ')' //平衡组末尾字符
if (!chompBalanced(queue[pos], next)){ if (!chompBalanced(queue[pos], next)) {
ruleTypeList.clear() ruleTypeList.clear()
rule.clear() rule.clear()
consumeToAny("<js>","@js:") consumeToAny("<js>", "@js:")
rule += queue.substring(0,pos) rule += queue.substring(0, pos)
ruleTypeList += queue.substring(pos, pos + 4) //设置组合类型 ruleTypeList += queue.substring(pos, pos + 4) //设置组合类型
} }
@ -472,7 +481,7 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
var isMulu = false var isMulu = false
var isreverse = false var isreverse = false
var isAllInOne= false var isAllInOne = false
var isFind = false var isFind = false
private val findName = ArrayList<String>() private val findName = ArrayList<String>()
@ -488,15 +497,15 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
private val defaultRuleType = 0 private val defaultRuleType = 0
@JvmOverloads @JvmOverloads
fun setContent(cont: String,type:String = ""): RuleAnalyzer { fun setContent(cont: String, type: String = ""): RuleAnalyzer {
queue = cont queue = cont
when(type){ when (type) {
"mulu" -> { "mulu" -> {
if(queue[0] =='-'){ //目录反转 if (queue[0] == '-') { //目录反转
isreverse = true isreverse = true
startX++ startX++
pos++ pos++
}else if(queue[0] =='?'){ //AllInOne } else if (queue[0] == '?') { //AllInOne
isAllInOne = true isAllInOne = true
startX++ startX++
pos++ pos++
@ -505,8 +514,8 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
} }
"find" -> { "find" -> {
pos = queue.indexOf("::") pos = queue.indexOf("::")
findName.add(queue.substring(startX,pos)) findName.add(queue.substring(startX, pos))
pos+=2 pos += 2
isFind = true isFind = true
} }
"url" -> { "url" -> {
@ -538,14 +547,15 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
/** /**
* 参数字符串 * 参数字符串
*/ */
private val STARTSTRURL = arrayOf(",{",) private val STARTSTRURL = arrayOf(",{")
private val regexPattern = Pattern.compile("\\$\\d{1,2}") private val regexPattern = Pattern.compile("\\$\\d{1,2}")
private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE) private val putPattern = Pattern.compile("@put:(\\{[^}]+?\\})", Pattern.CASE_INSENSITIVE)
private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE) private val getPattern = Pattern.compile("@get:\\{([^}]+?)\\}", Pattern.CASE_INSENSITIVE)
private val evalPattern = Pattern.compile("\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE) private val evalPattern =
Pattern.compile("\\{\\{[\\w\\W]*?\\}\\}", Pattern.CASE_INSENSITIVE)
val ENDSTR= mapOf( val ENDSTR = mapOf(
"<js>" to "</js>", "<js>" to "</js>",
"{{" to "}}", "{{" to "}}",
) )
@ -553,18 +563,35 @@ class RuleAnalyzer(data: String, code: Boolean = false) {
/** /**
* 规则起始字符串 * 规则起始字符串
*/ */
private val STARTSTR = arrayOf("@js:","<js>","</js>","##","@@","@" private val STARTSTR = arrayOf(
,"{{@", "{{","}}" "@js:",
,"}" "<js>",
, "{@", "{/", "{$" "</js>",
, "{class" "##",
, "{id" "@@",
, "{tag" "@",
, "{text" "{{@",
, "{children" "{{",
,"/","$","@xpath:","@json:","@css:" "}}",
,"||", "&&", "%%" "}",
,"@get:{","@put:{" "{@",
"{/",
"{$",
"{class",
"{id",
"{tag",
"{text",
"{children",
"/",
"$",
"@xpath:",
"@json:",
"@css:",
"||",
"&&",
"%%",
"@get:{",
"@put:{"
) )
/** /**

@ -415,10 +415,11 @@ object ReadBook {
else -> chapter.title else -> chapter.title
} }
val contents = contentProcessor!!.getContent(book, chapter.title, content) val contents = contentProcessor!!.getContent(book, chapter.title, content)
val textChapter = ChapterProvider.getTextChapter(book, chapter,contents,chapterSize) val textChapter =
ChapterProvider.getTextChapter(book, chapter, contents, chapterSize)
val offset = chapter.index - durChapterIndex val offset = chapter.index - durChapterIndex
if (upContent) callBack?.upContent(offset ,resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
when (offset) { when (offset) {
0 -> { 0 -> {
curTextChapter = textChapter curTextChapter = textChapter
@ -426,7 +427,7 @@ object ReadBook {
curPageChanged() curPageChanged()
callBack?.contentLoadFinish() callBack?.contentLoadFinish()
} }
- 1 -> { -1 -> {
prevTextChapter = textChapter prevTextChapter = textChapter
} }
1 -> { 1 -> {

@ -113,7 +113,7 @@ object ChapterProvider {
} else if (book.getImageStyle() != Book.imgStyleText) { } else if (book.getImageStyle() != Book.imgStyleText) {
content.replace(AppPattern.imgPattern.toRegex(), "\n\$0\n").split("\n") content.replace(AppPattern.imgPattern.toRegex(), "\n\$0\n").split("\n")
.forEach { text -> .forEach { text ->
if(text.isNotBlank()){ if (text.isNotBlank()) {
if (!text.startsWith("<img src=\"")) { //非图片 if (!text.startsWith("<img src=\"")) { //非图片
val isTitle = index == 0 val isTitle = index == 0
val textPaint = if (isTitle) titlePaint else contentPaint val textPaint = if (isTitle) titlePaint else contentPaint

Loading…
Cancel
Save