pull/2253/head
XieJC 2 years ago
commit 206382fa00
  1. 1692
      app/schemas/io.legado.app.data.AppDatabase/53.json
  2. 4
      app/src/main/assets/updateLog.md
  3. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 5
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  5. 3
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  6. 15
      app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt
  7. 23
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  8. 20
      app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt
  9. 68
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  10. 12
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  11. 6
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  12. 2
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  13. 7
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  14. 6
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  15. 183
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  16. 61
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  17. 45
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  18. 6
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  19. 3
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
  20. 7
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextColumn.kt
  21. 10
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  22. 10
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  23. 3
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt
  24. 53
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  25. 65
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  26. 28
      app/src/main/res/layout/activity_book_source_edit.xml
  27. 7
      app/src/main/res/menu/book_read.xml
  28. 11
      app/src/main/res/values-es-rES/strings.xml
  29. 11
      app/src/main/res/values-ja-rJP/strings.xml
  30. 11
      app/src/main/res/values-pt-rBR/strings.xml
  31. 11
      app/src/main/res/values-zh-rHK/strings.xml
  32. 11
      app/src/main/res/values-zh-rTW/strings.xml
  33. 11
      app/src/main/res/values-zh/strings.xml
  34. 36
      app/src/main/res/values/non_translat.xml
  35. 11
      app/src/main/res/values/strings.xml

File diff suppressed because it is too large Load Diff

@ -11,11 +11,11 @@
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源! * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
**2022/09/03** **2022/09/07**
* 更新cronet: 105.0.5195.79 * 更新cronet: 105.0.5195.79
* 更新cronet: 105.0.5195.77
* 更新web端书源编辑 by jgckM * 更新web端书源编辑 by jgckM
* 修复仿真翻页点击时翻页动画异常,化仿真翻页点击翻页效果 by 821938089
**2022/08/31** **2022/08/31**

@ -32,6 +32,7 @@ object PreferKey {
const val prevKeys = "prevKeyCodes" const val prevKeys = "prevKeyCodes"
const val nextKeys = "nextKeyCodes" const val nextKeys = "nextKeyCodes"
const val showDiscovery = "showDiscovery" const val showDiscovery = "showDiscovery"
const val enableReview = "enableReview"
const val showRss = "showRss" const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout" const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort" const val bookshelfSort = "bookshelfSort"

@ -20,7 +20,7 @@ val appDb by lazy {
} }
@Database( @Database(
version = 52, version = 53,
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,
@ -36,7 +36,8 @@ val appDb by lazy {
AutoMigration(from = 48, to = 49), AutoMigration(from = 48, to = 49),
AutoMigration(from = 49, to = 50), AutoMigration(from = 49, to = 50),
AutoMigration(from = 50, to = 51), AutoMigration(from = 50, to = 51),
AutoMigration(from = 51, to = 52) AutoMigration(from = 51, to = 52),
AutoMigration(from = 52, to = 53)
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

@ -50,6 +50,9 @@ interface BookSourceDao {
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc") @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
fun flowExplore(): Flow<List<BookSource>> fun flowExplore(): Flow<List<BookSource>>
// @Query("select * from book_sources where enabledReview = 1 order by customOrder asc")
// fun flowReview(): Flow<List<BookSource>>
@Query("select * from book_sources where loginUrl is not null and loginUrl != ''") @Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
fun flowLogin(): Flow<List<BookSource>> fun flowLogin(): Flow<List<BookSource>>

@ -0,0 +1,15 @@
package io.legado.app.data.entities
import android.os.Parcelable
import androidx.room.ColumnInfo
import kotlinx.parcelize.Parcelize
@Parcelize
class BookChapterReview(
@ColumnInfo(defaultValue = "0")
var bookId: Long = 0,
var chapterId: Long = 0,
var summaryUrl: String = "",
): Parcelable {
}

@ -37,7 +37,9 @@ data class BookSource(
// 是否启用 // 是否启用
var enabled: Boolean = true, var enabled: Boolean = true,
// 启用发现 // 启用发现
var enabledExplore: Boolean = true, var enabledExplore: Boolean = false,
// 启用段评
var enabledReview: Boolean? = false,
// 启用okhttp CookieJAr 自动保存每次请求的cookie // 启用okhttp CookieJAr 自动保存每次请求的cookie
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
override var enabledCookieJar: Boolean? = false, override var enabledCookieJar: Boolean? = false,
@ -74,7 +76,9 @@ data class BookSource(
// 目录页规则 // 目录页规则
var ruleToc: TocRule? = null, var ruleToc: TocRule? = null,
// 正文页规则 // 正文页规则
var ruleContent: ContentRule? = null var ruleContent: ContentRule? = null,
// 段评规则
var ruleReview: ReviewRule? = null
) : Parcelable, BaseSource { ) : Parcelable, BaseSource {
override fun getTag(): String { override fun getTag(): String {
@ -170,6 +174,13 @@ data class BookSource(
return rule return rule
} }
fun getReviewRule(): ReviewRule {
ruleReview?.let { return it }
val rule = ReviewRule()
ruleReview = rule
return rule
}
fun getDisPlayNameGroup(): String { fun getDisPlayNameGroup(): String {
return if (bookSourceGroup.isNullOrBlank()) { return if (bookSourceGroup.isNullOrBlank()) {
bookSourceName bookSourceName
@ -303,5 +314,13 @@ data class BookSource(
fun stringToContentRule(json: String?) = fun stringToContentRule(json: String?) =
GSON.fromJsonObject<ContentRule>(json).getOrNull() GSON.fromJsonObject<ContentRule>(json).getOrNull()
@TypeConverter
fun stringToReviewRule(json: String?) =
GSON.fromJsonObject<ReviewRule>(json).getOrNull()
@TypeConverter
fun reviewRuleToString(reviewRule: ReviewRule?): String =
GSON.toJson(reviewRule)
} }
} }

@ -0,0 +1,20 @@
package io.legado.app.data.entities.rule
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class ReviewRule(
var reviewUrl: String? = null, // 段评URL
var avatarRule: String? = null, // 段评发布者头像
var contentRule: String? = null, // 段评内容
var postTimeRule: String? = null, // 段评发布时间
var reviewQuoteUrl: String? = null, // 获取段评回复URL
// 这些功能将在以上功能完成以后实现
var voteUpUrl: String? = null, // 点赞URL
var voteDownUrl: String? = null, // 点踩URL
var postReviewUrl: String? = null, // 发送回复URL
var postQuoteUrl: String? = null, // 发送回复段评URL
var deleteUrl: String? = null, // 删除段评URL
): Parcelable

@ -27,6 +27,7 @@ import splitties.init.appCtx
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.Charset import java.nio.charset.Charset
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -125,7 +126,7 @@ interface JsExtensions {
html = html, html = html,
javaScript = js, javaScript = js,
headerMap = getSource()?.getHeaderMap(true), headerMap = getSource()?.getHeaderMap(true),
tag = getSource()?.getKey() tag = getSource()?.getKey()
).getStrResponse().body ).getStrResponse().body
} }
} }
@ -143,15 +144,18 @@ interface JsExtensions {
* 使用内置浏览器打开链接并等待网页结果 * 使用内置浏览器打开链接并等待网页结果
*/ */
fun startBrowserAwait(url: String, title: String): StrResponse { fun startBrowserAwait(url: String, title: String): StrResponse {
return StrResponse(url, SourceVerificationHelp.getVerificationResult(getSource(), url, title, true)) return StrResponse(
url,
SourceVerificationHelp.getVerificationResult(getSource(), url, title, true)
)
} }
/** /**
* 打开图片验证码对话框等待返回验证结果 * 打开图片验证码对话框等待返回验证结果
*/ */
fun getVerificationCode(imageUrl: String): String { fun getVerificationCode(imageUrl: String): String {
return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false) return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
} }
/** /**
* 可从网络本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本 * 可从网络本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本
@ -203,6 +207,29 @@ interface JsExtensions {
} }
} }
/**
* 下载文件
* @param url 下载地址:可带参数type,文件后缀,不带默认zip
* @return 下载的文件相对路径
*/
fun downloadFile(url: String): String {
val analyzeUrl = AnalyzeUrl(url, source = getSource())
val type = analyzeUrl.type ?: "zip"
val path = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}"
)
FileUtils.delete(path)
analyzeUrl.getInputStream().use { iStream ->
val file = FileUtils.createFileIfNotExist(path)
FileOutputStream(file).use { oStream ->
iStream.copyTo(oStream)
}
}
return path.substring(FileUtils.getCachePath().length)
}
/** /**
* 实现16进制字符串转文件 * 实现16进制字符串转文件
* @param content 需要转成文件的16进制字符串 * @param content 需要转成文件的16进制字符串
@ -211,18 +238,18 @@ interface JsExtensions {
*/ */
fun downloadFile(content: String, url: String): String { fun downloadFile(content: String, url: String): String {
val type = AnalyzeUrl(url, source = getSource()).type ?: return "" val type = AnalyzeUrl(url, source = getSource()).type ?: return ""
val zipPath = FileUtils.getPath( val path = FileUtils.getPath(
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()), FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}" "${MD5Utils.md5Encode16(url)}.${type}"
) )
FileUtils.delete(zipPath) FileUtils.delete(path)
val zipFile = FileUtils.createFileIfNotExist(zipPath) val file = FileUtils.createFileIfNotExist(path)
StringUtils.hexStringToByte(content).let { StringUtils.hexStringToByte(content).let {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
zipFile.writeBytes(it) file.writeBytes(it)
} }
} }
return zipPath.substring(FileUtils.getCachePath().length) return path.substring(FileUtils.getCachePath().length)
} }
/** /**
@ -433,13 +460,15 @@ interface JsExtensions {
} }
/** /**
* js实现文件夹内所有文件读取 * js实现文件夹内所有文本文件读取
* @param path 文件夹相对路径
* @return 所有文件字符串换行连接
*/ */
fun getTxtInFolder(unzipPath: String): String { fun getTxtInFolder(path: String): String {
if (unzipPath.isEmpty()) return "" if (path.isEmpty()) return ""
val unzipFolder = getFile(unzipPath) val folder = getFile(path)
val contents = StringBuilder() val contents = StringBuilder()
unzipFolder.listFiles().let { folder.listFiles().let {
if (it != null) { if (it != null) {
for (f in it) { for (f in it) {
val charsetName = EncodingDetect.getEncode(f) val charsetName = EncodingDetect.getEncode(f)
@ -449,7 +478,7 @@ interface JsExtensions {
contents.deleteCharAt(contents.length - 1) contents.deleteCharAt(contents.length - 1)
} }
} }
FileUtils.delete(unzipFolder.absolutePath) FileUtils.delete(folder.absolutePath)
return contents.toString() return contents.toString()
} }
@ -833,7 +862,7 @@ interface JsExtensions {
} }
fun desBase64DecodeToString( fun desBase64DecodeToString(
data: String, key: String, transformation: String, iv: String data: String, key: String, transformation: String, iv: String
): String? { ): String? {
return EncoderUtils.decryptBase64DES( return EncoderUtils.decryptBase64DES(
data.encodeToByteArray(), data.encodeToByteArray(),
@ -1028,7 +1057,10 @@ interface JsExtensions {
algorithm: String, algorithm: String,
key: String key: String
): String { ): String {
return Base64.encodeToString(HMac(algorithm, key.toByteArray()).digest(data), Base64.NO_WRAP) return Base64.encodeToString(
HMac(algorithm, key.toByteArray()).digest(data),
Base64.NO_WRAP
)
} }
fun md5Encode(str: String): String { fun md5Encode(str: String): String {

@ -151,6 +151,7 @@ object SourceAnalyzer {
source.enabled = sourceAny.enabled source.enabled = sourceAny.enabled
source.enabledExplore = sourceAny.enabledExplore source.enabledExplore = sourceAny.enabledExplore
source.enabledCookieJar = sourceAny.enabledCookieJar source.enabledCookieJar = sourceAny.enabledCookieJar
source.enabledReview = sourceAny.enabledReview
source.concurrentRate = sourceAny.concurrentRate source.concurrentRate = sourceAny.concurrentRate
source.header = sourceAny.header source.header = sourceAny.header
source.loginUrl = when (sourceAny.loginUrl) { source.loginUrl = when (sourceAny.loginUrl) {
@ -206,6 +207,13 @@ object SourceAnalyzer {
GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent)) GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent))
.getOrNull() .getOrNull()
} }
source.ruleReview = if (sourceAny.ruleReview is String) {
GSON.fromJsonObject<ReviewRule>(sourceAny.ruleReview.toString())
.getOrNull()
} else {
GSON.fromJsonObject<ReviewRule>(GSON.toJson(sourceAny.ruleReview))
.getOrNull()
}
} }
source source
} }
@ -221,6 +229,7 @@ object SourceAnalyzer {
var customOrder: Int = 0, // 手动排序编号 var customOrder: Int = 0, // 手动排序编号
var enabled: Boolean = true, // 是否启用 var enabled: Boolean = true, // 是否启用
var enabledExplore: Boolean = true, // 启用发现 var enabledExplore: Boolean = true, // 启用发现
var enabledReview: Boolean = false, // 启用段评
var enabledCookieJar: Boolean = false, // 启用CookieJar var enabledCookieJar: Boolean = false, // 启用CookieJar
var concurrentRate: String? = null, // 并发率 var concurrentRate: String? = null, // 并发率
var header: String? = null, // 请求头 var header: String? = null, // 请求头
@ -238,7 +247,8 @@ object SourceAnalyzer {
var ruleSearch: Any? = null, // 搜索规则 var ruleSearch: Any? = null, // 搜索规则
var ruleBookInfo: Any? = null, // 书籍信息页规则 var ruleBookInfo: Any? = null, // 书籍信息页规则
var ruleToc: Any? = null, // 目录页规则 var ruleToc: Any? = null, // 目录页规则
var ruleContent: Any? = null // 正文页规则 var ruleContent: Any? = null, // 正文页规则
var ruleReview: Any? = null // 段评规则
) )
// default规则适配 // default规则适配

@ -154,6 +154,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val autoRefreshBook: Boolean val autoRefreshBook: Boolean
get() = appCtx.getPrefBoolean(PreferKey.autoRefresh) get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
var enableReview: Boolean
get() = appCtx.getPrefBoolean(PreferKey.enableReview, false)
set(value) {
appCtx.putPrefBoolean(PreferKey.enableReview, value)
}
var threadCount: Int var threadCount: Int
get() = appCtx.getPrefInt(PreferKey.threadCount, 16) get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
set(value) { set(value) {

@ -449,7 +449,7 @@ object ReadBookConfig {
var letterSpacing: Float = 0.1f,//字间距 var letterSpacing: Float = 0.1f,//字间距
var lineSpacingExtra: Int = 12,//行间距 var lineSpacingExtra: Int = 12,//行间距
var paragraphSpacing: Int = 2,//段距 var paragraphSpacing: Int = 2,//段距
var titleMode: Int = 0,//标题居中 var titleMode: Int = 0,//标题位置 0:居左 1:居中 2:隐藏
var titleSize: Int = 0, var titleSize: Int = 0,
var titleTopSpacing: Int = 0, var titleTopSpacing: Int = 0,
var titleBottomSpacing: Int = 0, var titleBottomSpacing: Int = 0,

@ -300,6 +300,13 @@ object WebBook {
} }
} }
/**
* 获取段评
*/
fun getReview() {
// TODO
}
/** /**
* 精准搜索 * 精准搜索
*/ */

@ -256,6 +256,7 @@ class ReadBookActivity : BaseReadBookActivity(),
else -> when (item.itemId) { else -> when (item.itemId) {
R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule()
R.id.menu_re_segment -> item.isChecked = book.getReSegment() R.id.menu_re_segment -> item.isChecked = book.getReSegment()
R.id.menu_enable_review -> item.isChecked = AppConfig.enableReview
R.id.menu_reverse_content -> item.isVisible = onLine R.id.menu_reverse_content -> item.isVisible = onLine
} }
} }
@ -356,6 +357,11 @@ class ReadBookActivity : BaseReadBookActivity(),
menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment() menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment()
ReadBook.loadContent(false) ReadBook.loadContent(false)
} }
R.id.menu_enable_review -> {
AppConfig.enableReview = !AppConfig.enableReview
menu?.findItem(R.id.menu_enable_review)?.isChecked = AppConfig.enableReview
ReadBook.loadContent(false)
}
R.id.menu_page_anim -> showPageAnimConfig { R.id.menu_page_anim -> showPageAnimConfig {
binding.readView.upPageAnim() binding.readView.upPageAnim()
ReadBook.loadContent(false) ReadBook.loadContent(false)

@ -4,6 +4,8 @@ import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import android.text.StaticLayout
import android.text.TextPaint
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import io.legado.app.R import io.legado.app.R
@ -14,10 +16,7 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.accentColor
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.page.entities.TextChar import io.legado.app.ui.book.read.page.entities.*
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.TextPos
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.ImageProvider import io.legado.app.ui.book.read.page.provider.ImageProvider
import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
@ -28,7 +27,7 @@ import io.legado.app.utils.toastOnUi
import kotlin.math.min import kotlin.math.min
/** /**
* 阅读内容界面 * 阅读内容视图
*/ */
class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true) var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
@ -65,12 +64,18 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
callBack = activity as CallBack callBack = activity as CallBack
} }
/**
* 设置内容
*/
fun setContent(textPage: TextPage) { fun setContent(textPage: TextPage) {
this.textPage = textPage this.textPage = textPage
imagePaint.isAntiAlias = AppConfig.useAntiAlias imagePaint.isAntiAlias = AppConfig.useAntiAlias
invalidate() invalidate()
} }
/**
* 更新绘制区域
*/
fun upVisibleRect() { fun upVisibleRect() {
visibleRect.set( visibleRect.set(
ChapterProvider.paddingLeft.toFloat(), ChapterProvider.paddingLeft.toFloat(),
@ -121,6 +126,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
} }
/**
* 绘制页面
*/
private fun draw( private fun draw(
canvas: Canvas, canvas: Canvas,
textPage: TextPage, textPage: TextPage,
@ -150,15 +158,90 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
ChapterProvider.contentPaint ChapterProvider.contentPaint
} }
val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
val linePaint = Paint()
linePaint.strokeWidth = textPaint.textSize / 21
linePaint.color = textColor
val reviewCountPaint = TextPaint()
reviewCountPaint.textSize = textPaint.textSize * 0.6F
reviewCountPaint.color = textColor
textLine.textChars.forEach { textLine.textChars.forEach {
if (it.isImage) { when (it.style) {
drawImage(canvas, textPage, textLine, it, lineTop, lineBottom) 0 -> {
} else { textPaint.color = textColor
textPaint.color = textColor if (it.isSearchResult) {
if (it.isSearchResult) { textPaint.color = context.accentColor
textPaint.color = context.accentColor }
canvas.drawText(it.charData, it.start, lineBase, textPaint)
}
1 -> drawImage(canvas, textPage, textLine, it, lineTop, lineBottom)
2 -> {
if (textLine.reviewCount <= 0) return@forEach
canvas.drawLine(
it.start,
lineBase - textPaint.textSize * 2 / 5,
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize / 4,
linePaint
)
canvas.drawLine(
it.start,
lineBase - textPaint.textSize * 0.38F,
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize * 0.55F,
linePaint
)
canvas.drawLine(
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize / 4,
it.start + textPaint.textSize / 6,
lineBase,
linePaint
)
canvas.drawLine(
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize * 0.55F,
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize * 0.8F,
linePaint
)
canvas.drawLine(
it.start + textPaint.textSize / 6,
lineBase,
it.start + textPaint.textSize * 1.6F,
lineBase,
linePaint
)
canvas.drawLine(
it.start + textPaint.textSize / 6,
lineBase - textPaint.textSize * 0.8F,
it.start + textPaint.textSize * 1.6F,
lineBase - textPaint.textSize * 0.8F,
linePaint
)
canvas.drawLine(
it.start + textPaint.textSize * 1.6F,
lineBase - textPaint.textSize * 0.8F,
it.start + textPaint.textSize * 1.6F,
lineBase,
linePaint
)
if (textLine.reviewCount < 100) canvas.drawText(
textLine.reviewCount.toString(),
it.start + textPaint.textSize * 0.87F -
StaticLayout.getDesiredWidth(
textLine.reviewCount.toString(),
reviewCountPaint
) / 2,
lineBase - textPaint.textSize / 6,
reviewCountPaint
)
else canvas.drawText(
"99+",
it.start + textPaint.textSize * 0.35F,
lineBase - textPaint.textSize / 6,
reviewCountPaint
)
} }
canvas.drawText(it.charData, it.start, lineBase, textPaint)
} }
if (it.selected) { if (it.selected) {
canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint) canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint)
@ -174,7 +257,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
canvas: Canvas, canvas: Canvas,
textPage: TextPage, textPage: TextPage,
textLine: TextLine, textLine: TextLine,
textChar: TextChar, textChar: TextColumn,
lineTop: Float, lineTop: Float,
lineBottom: Float lineBottom: Float
) { ) {
@ -259,6 +342,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
invalidate() invalidate()
} }
/**
* 重置滚动位置
*/
fun resetPageOffset() { fun resetPageOffset() {
pageOffset = 0 pageOffset = 0
} }
@ -269,18 +355,34 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
fun longPress( fun longPress(
x: Float, x: Float,
y: Float, y: Float,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit, select: (textPos: TextPos) -> Unit,
) { ) {
touch(x, y) { _, relativePos, _, lineIndex, _, charIndex, textChar -> touch(x, y) { _, textPos, _, _, textColumn ->
if (textChar.isImage) { if (textColumn.style == 2) return@touch
callBack.onImageLongPress(x, y, textChar.charData) if (textColumn.style == 1) {
callBack.onImageLongPress(x, y, textColumn.charData)
} else { } else {
if (!selectAble) return@touch if (!selectAble) return@touch
textChar.selected = true textColumn.selected = true
invalidate() invalidate()
select(relativePos, lineIndex, charIndex) select(textPos)
}
}
}
/**
* 单击
* @return true:已处理, false:未处理
*/
fun click(x: Float, y: Float): Boolean {
var handled = false
touch(x, y) { _, textPos, textPage, textLine, textColumn ->
if (textColumn.style == 2) {
context.toastOnUi("Button Pressed!")
handled = true
} }
} }
return handled
} }
/** /**
@ -289,12 +391,13 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
fun selectText( fun selectText(
x: Float, x: Float,
y: Float, y: Float,
select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit, select: (textPos: TextPos) -> Unit,
) { ) {
touch(x, y) { _, relativePos, _, lineIndex, _, charIndex, textChar -> touch(x, y) { _, textPos, _, _, textColumn ->
textChar.selected = true if (textColumn.style == 2) return@touch
textColumn.selected = true
invalidate() invalidate()
select(relativePos, lineIndex, charIndex) select(textPos)
} }
} }
@ -302,11 +405,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
* 开始选择符移动 * 开始选择符移动
*/ */
fun selectStartMove(x: Float, y: Float) { fun selectStartMove(x: Float, y: Float) {
touch(x, y) { relativeOffset, relativePos, _, lineIndex, textLine, charIndex, textChar -> touch(x, y) { relativeOffset, textPos, _, textLine, textChar ->
val pos = TextPos(relativePos, lineIndex, charIndex) if (selectStart.compare(textPos) != 0) {
if (selectStart.compare(pos) != 0) { if (textPos.compare(selectEnd) <= 0) {
if (pos.compare(selectEnd) <= 0) { selectStart.upData(pos = textPos)
selectStart.upData(pos = pos)
upSelectedStart( upSelectedStart(
textChar.start, textChar.start,
textLine.lineBottom + relativeOffset, textLine.lineBottom + relativeOffset,
@ -322,11 +424,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
* 结束选择符移动 * 结束选择符移动
*/ */
fun selectEndMove(x: Float, y: Float) { fun selectEndMove(x: Float, y: Float) {
touch(x, y) { relativeOffset, relativePos, _, lineIndex, textLine, charIndex, textChar -> touch(x, y) { relativeOffset, textPos, _, textLine, textChar ->
val pos = TextPos(relativePos, lineIndex, charIndex) if (textPos.compare(selectEnd) != 0) {
if (pos.compare(selectEnd) != 0) { if (textPos.compare(selectStart) >= 0) {
if (pos.compare(selectStart) >= 0) { selectEnd.upData(textPos)
selectEnd.upData(pos)
upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
upSelectChars() upSelectChars()
} }
@ -334,17 +435,19 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
} }
} }
/**
* 触碰位置信息
* @param touched 回调
*/
private fun touch( private fun touch(
x: Float, x: Float,
y: Float, y: Float,
touched: ( touched: (
relativeOffset: Float, relativeOffset: Float,
relativePos: Int, textPos: TextPos,
textPage: TextPage, textPage: TextPage,
lineIndex: Int,
textLine: TextLine, textLine: TextLine,
charIndex: Int, textColumn: TextColumn
textChar: TextChar
) -> Unit ) -> Unit
) { ) {
if (!visibleRect.contains(x, y)) return if (!visibleRect.contains(x, y)) return
@ -363,9 +466,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
if (textChar.isTouch(x)) { if (textChar.isTouch(x)) {
touched.invoke( touched.invoke(
relativeOffset, relativeOffset,
relativePos, textPage, TextPos(relativePos, lineIndex, charIndex),
lineIndex, textLine, textPage, textLine, textChar
charIndex, textChar
) )
return return
} }
@ -415,6 +517,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
textPos.lineIndex = lineIndex textPos.lineIndex = lineIndex
for ((charIndex, textChar) in textLine.textChars.withIndex()) { for ((charIndex, textChar) in textLine.textChars.withIndex()) {
textPos.charIndex = charIndex textPos.charIndex = charIndex
if (textChar.style == 2) continue
textChar.selected = textChar.selected =
textPos.compare(selectStart) >= 0 && textPos.compare(selectEnd) <= 0 textPos.compare(selectStart) >= 0 && textPos.compare(selectEnd) <= 0
textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult

@ -14,6 +14,7 @@ import io.legado.app.help.config.ReadTipConfig
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.ui.book.read.page.entities.TextPage
import io.legado.app.ui.book.read.page.entities.TextPos
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.widget.BatteryView import io.legado.app.ui.widget.BatteryView
import io.legado.app.utils.activity import io.legado.app.utils.activity
@ -24,7 +25,7 @@ import splitties.views.backgroundColor
import java.util.* import java.util.*
/** /**
* 阅读界面 * 页面视图
*/ */
class PageView(context: Context) : FrameLayout(context) { class PageView(context: Context) : FrameLayout(context) {
@ -104,6 +105,9 @@ class PageView(context: Context) : FrameLayout(context) {
isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true
} }
/**
* 更新阅读信息
*/
private fun upTipStyle() = binding.run { private fun upTipStyle() = binding.run {
tvHeaderLeft.tag = null tvHeaderLeft.tag = null
tvHeaderMiddle.tag = null tvHeaderMiddle.tag = null
@ -189,6 +193,10 @@ class PageView(context: Context) : FrameLayout(context) {
} }
} }
/**
* 获取信息视图
* @param tip 信息类型
*/
private fun getTipView(tip: Int): BatteryView? = binding.run { private fun getTipView(tip: Int): BatteryView? = binding.run {
return when (tip) { return when (tip) {
ReadTipConfig.tipHeaderLeft -> tvHeaderLeft ReadTipConfig.tipHeaderLeft -> tvHeaderLeft
@ -201,6 +209,9 @@ class PageView(context: Context) : FrameLayout(context) {
} }
} }
/**
* 更新背景
*/
fun upBg() { fun upBg() {
if (ReadBookConfig.bgAlpha < 100) { if (ReadBookConfig.bgAlpha < 100) {
binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor
@ -211,15 +222,24 @@ class PageView(context: Context) : FrameLayout(context) {
upBgAlpha() upBgAlpha()
} }
/**
* 更新背景透明度
*/
fun upBgAlpha() { fun upBgAlpha() {
binding.vwBg.alpha = ReadBookConfig.bgAlpha / 100f binding.vwBg.alpha = ReadBookConfig.bgAlpha / 100f
} }
/**
* 更新时间信息
*/
fun upTime() { fun upTime() {
tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) tvTime?.text = timeFormat.format(Date(System.currentTimeMillis()))
upTimeBattery() upTimeBattery()
} }
/**
* 更新电池信息
*/
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun upBattery(battery: Int) { fun upBattery(battery: Int) {
this.battery = battery this.battery = battery
@ -228,6 +248,9 @@ class PageView(context: Context) : FrameLayout(context) {
upTimeBattery() upTimeBattery()
} }
/**
* 更新电池信息
*/
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun upTimeBattery() { private fun upTimeBattery() {
val time = timeFormat.format(Date(System.currentTimeMillis())) val time = timeFormat.format(Date(System.currentTimeMillis()))
@ -235,6 +258,9 @@ class PageView(context: Context) : FrameLayout(context) {
tvTimeBatteryP?.text = "$time $battery%" tvTimeBatteryP?.text = "$time $battery%"
} }
/**
* 设置内容
*/
fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
setProgress(textPage) setProgress(textPage)
if (resetPageOffset) { if (resetPageOffset) {
@ -243,14 +269,23 @@ class PageView(context: Context) : FrameLayout(context) {
binding.contentTextView.setContent(textPage) binding.contentTextView.setContent(textPage)
} }
/**
* 设置无障碍文本
*/
fun setContentDescription(content: String) { fun setContentDescription(content: String) {
binding.contentTextView.contentDescription = content binding.contentTextView.contentDescription = content
} }
/**
* 重置滚动位置
*/
fun resetPageOffset() { fun resetPageOffset() {
binding.contentTextView.resetPageOffset() binding.contentTextView.resetPageOffset()
} }
/**
* 设置进度
*/
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun setProgress(textPage: TextPage) = textPage.apply { fun setProgress(textPage: TextPage) = textPage.apply {
tvBookName?.text = ReadBook.book?.name tvBookName?.text = ReadBook.book?.name
@ -260,24 +295,44 @@ class PageView(context: Context) : FrameLayout(context) {
tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress"
} }
/**
* 滚动事件
*/
fun scroll(offset: Int) { fun scroll(offset: Int) {
binding.contentTextView.scroll(offset) binding.contentTextView.scroll(offset)
} }
/**
* 更新是否开启选择功能
*/
fun upSelectAble(selectAble: Boolean) { fun upSelectAble(selectAble: Boolean) {
binding.contentTextView.selectAble = selectAble binding.contentTextView.selectAble = selectAble
} }
/**
* 优先处理页面内单击
* @return true:已处理, false:未处理
*/
fun onClick(x: Float, y: Float): Boolean {
return binding.contentTextView.click(x, y - headerHeight)
}
/**
* 长按事件
*/
fun longPress( fun longPress(
x: Float, y: Float, x: Float, y: Float,
select: (relativePagePos: Int, lineIndex: Int, charIndex: Int) -> Unit, select: (textPos: TextPos) -> Unit,
) { ) {
return binding.contentTextView.longPress(x, y - headerHeight, select) return binding.contentTextView.longPress(x, y - headerHeight, select)
} }
/**
* 选择文本
*/
fun selectText( fun selectText(
x: Float, y: Float, x: Float, y: Float,
select: (relativePagePos: Int, lineIndex: Int, charIndex: Int) -> Unit, select: (textPos: TextPos) -> Unit,
) { ) {
return binding.contentTextView.selectText(x, y - headerHeight, select) return binding.contentTextView.selectText(x, y - headerHeight, select)
} }

@ -28,11 +28,14 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.invisible import io.legado.app.utils.invisible
import io.legado.app.utils.screenshot import io.legado.app.utils.screenshot
import io.legado.app.utils.toastOnUi
import java.text.BreakIterator import java.text.BreakIterator
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
/**
* 阅读视图
*/
class ReadView(context: Context, attrs: AttributeSet) : class ReadView(context: Context, attrs: AttributeSet) :
FrameLayout(context, attrs), FrameLayout(context, attrs),
DataSource { DataSource {
@ -222,7 +225,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
pressDown = false pressDown = false
if (!isPageMove) { if (!isPageMove) {
if (!longPressed && !pressOnTextSelected) { if (!longPressed && !pressOnTextSelected) {
onSingleTapUp() if (!curPage.onClick(startX, startY)) {
onSingleTapUp()
}
return true return true
} }
} }
@ -292,18 +297,18 @@ class ReadView(context: Context, attrs: AttributeSet) :
*/ */
private fun onLongPress() { private fun onLongPress() {
kotlin.runCatching { kotlin.runCatching {
curPage.longPress(startX, startY) { relativePos, lineIndex, charIndex -> curPage.longPress(startX, startY) { textPos: TextPos ->
isTextSelected = true isTextSelected = true
pressOnTextSelected = true pressOnTextSelected = true
initialTextPos.upData(relativePos, lineIndex, charIndex) initialTextPos.upData(textPos)
val startPos = TextPos(relativePos, lineIndex, charIndex) val startPos = textPos.copy()
val endPos = TextPos(relativePos, lineIndex, charIndex) val endPos = textPos.copy()
val page = curPage.relativePage(relativePos) val page = curPage.relativePage(textPos.relativePagePos)
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
var cIndex = charIndex var cIndex = textPos.charIndex
var lineStart = lineIndex var lineStart = textPos.lineIndex
var lineEnd = lineIndex var lineEnd = textPos.lineIndex
for (index in lineIndex - 1 downTo 0) { for (index in textPos.lineIndex - 1 downTo 0) {
val textLine = page.getLine(index) val textLine = page.getLine(index)
if (textLine.isParagraphEnd) { if (textLine.isParagraphEnd) {
break break
@ -313,7 +318,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
cIndex += textLine.charSize cIndex += textLine.charSize
} }
} }
for (index in lineIndex until page.lineSize) { for (index in textPos.lineIndex until page.lineSize) {
val textLine = page.getLine(index) val textLine = page.getLine(index)
stringBuilder.append(textLine.text) stringBuilder.append(textLine.text)
lineEnd += 1 lineEnd += 1
@ -419,11 +424,15 @@ class ReadView(context: Context, attrs: AttributeSet) :
* 选择文本 * 选择文本
*/ */
private fun selectText(x: Float, y: Float) { private fun selectText(x: Float, y: Float) {
curPage.selectText(x, y) { relativePagePos, lineIndex, charIndex -> curPage.selectText(x, y) { textPos ->
val compare = initialTextPos.compare(relativePagePos, lineIndex, charIndex) val compare = initialTextPos.compare(textPos)
when { when {
compare >= 0 -> { compare >= 0 -> {
curPage.selectStartMoveIndex(relativePagePos, lineIndex, charIndex) curPage.selectStartMoveIndex(
textPos.relativePagePos,
textPos.lineIndex,
textPos.charIndex
)
curPage.selectEndMoveIndex( curPage.selectEndMoveIndex(
initialTextPos.relativePagePos, initialTextPos.relativePagePos,
initialTextPos.lineIndex, initialTextPos.lineIndex,
@ -436,7 +445,11 @@ class ReadView(context: Context, attrs: AttributeSet) :
initialTextPos.lineIndex, initialTextPos.lineIndex,
initialTextPos.charIndex initialTextPos.charIndex
) )
curPage.selectEndMoveIndex(relativePagePos, lineIndex, charIndex) curPage.selectEndMoveIndex(
textPos.relativePagePos,
textPos.lineIndex,
textPos.charIndex
)
} }
} }
} }

@ -119,7 +119,11 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
abortAnim() abortAnim()
if (!hasNext()) return if (!hasNext()) return
setDirection(PageDirection.NEXT) setDirection(PageDirection.NEXT)
readView.setStartPoint(viewWidth.toFloat(), 1f, false) val y = when {
viewHeight / 2 < startY -> viewHeight.toFloat() * 0.9f
else -> 1f
}
readView.setStartPoint(viewWidth.toFloat() * 0.9f, y, false)
onAnimStart(animationSpeed) onAnimStart(animationSpeed)
} }

@ -2,6 +2,9 @@ package io.legado.app.ui.book.read.page.entities
import kotlin.math.min import kotlin.math.min
/**
* 章节信息
*/
@Suppress("unused") @Suppress("unused")
data class TextChapter( data class TextChapter(
val position: Int, val position: Int,

@ -1,11 +1,14 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
data class TextChar( /**
* 字符信息
*/
data class TextColumn(
val charData: String, val charData: String,
var start: Float, var start: Float,
var end: Float, var end: Float,
val style: Int = 0, //0:文字,1:图片,2:按钮
var selected: Boolean = false, var selected: Boolean = false,
var isImage: Boolean = false,
var isSearchResult: Boolean = false var isSearchResult: Boolean = false
) { ) {

@ -4,10 +4,14 @@ import android.text.TextPaint
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.textHeight import io.legado.app.utils.textHeight
/**
* 行信息
*/
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused", "MemberVisibilityCanBePrivate")
data class TextLine( data class TextLine(
var text: String = "", var text: String = "",
val textChars: ArrayList<TextChar> = arrayListOf(), val textChars: ArrayList<TextColumn> = arrayListOf(),
val reviewCount: Int = 0,
var lineTop: Float = 0f, var lineTop: Float = 0f,
var lineBase: Float = 0f, var lineBase: Float = 0f,
var lineBottom: Float = 0f, var lineBottom: Float = 0f,
@ -27,13 +31,13 @@ data class TextLine(
lineBase = lineBottom - textPaint.fontMetrics.descent lineBase = lineBottom - textPaint.fontMetrics.descent
} }
fun getTextChar(index: Int): TextChar { fun getTextChar(index: Int): TextColumn {
return textChars.getOrElse(index) { return textChars.getOrElse(index) {
textChars.last() textChars.last()
} }
} }
fun getTextCharReverseAt(index: Int): TextChar { fun getTextCharReverseAt(index: Int): TextColumn {
return textChars[textChars.lastIndex - index] return textChars[textChars.lastIndex - index]
} }

@ -11,6 +11,9 @@ import splitties.init.appCtx
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.min import kotlin.math.min
/**
* 页面信息
*/
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused", "MemberVisibilityCanBePrivate")
data class TextPage( data class TextPage(
var index: Int = 0, var index: Int = 0,
@ -111,7 +114,12 @@ data class TextPage(
val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint) val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
val x1 = x + cw val x1 = x + cw
textLine.textChars.add( textLine.textChars.add(
TextChar(char, start = x, end = x1) TextColumn(
char,
start = x,
end = x1,
style = if (textLine.text.length - 1 == index && char == "\uD83D\uDCAC") 2 else 0
)
) )
x = x1 x = x1
} }

@ -1,5 +1,8 @@
package io.legado.app.ui.book.read.page.entities package io.legado.app.ui.book.read.page.entities
/**
* 位置信息
*/
data class TextPos( data class TextPos(
var relativePagePos: Int, var relativePagePos: Int,
var lineIndex: Int, var lineIndex: Int,

@ -14,7 +14,7 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.model.ReadBook import io.legado.app.model.ReadBook
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.TextChar import io.legado.app.ui.book.read.page.entities.TextColumn
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
import io.legado.app.utils.* import io.legado.app.utils.*
@ -116,7 +116,13 @@ object ChapterProvider {
if (ReadBookConfig.titleMode != 2) { if (ReadBookConfig.titleMode != 2) {
displayTitle.splitNotBlank("\n").forEach { text -> displayTitle.splitNotBlank("\n").forEach { text ->
setTypeText( setTypeText(
book, absStartX, durY, text, textPages, stringBuilder, titlePaint, book,
absStartX,
durY,
if (AppConfig.enableReview) text + "\ud83d\udcac" else text,
textPages,
stringBuilder,
titlePaint,
isTitle = true, isTitle = true,
isTitleWithNoContent = contents.isEmpty(), isTitleWithNoContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume isVolumeTitle = bookChapter.isVolume
@ -171,7 +177,13 @@ object ChapterProvider {
val text = content.substring(start, content.length) val text = content.substring(start, content.length)
if (text.isNotBlank()) { if (text.isNotBlank()) {
setTypeText( setTypeText(
book, absStartX, durY, text, textPages, stringBuilder, contentPaint book,
absStartX,
durY,
if (AppConfig.enableReview) text + "\ud83d\udcac" else text,
textPages,
stringBuilder,
contentPaint
).let { ).let {
absStartX = it.first absStartX = it.first
durY = it.second durY = it.second
@ -252,7 +264,7 @@ object ChapterProvider {
Pair(0f, width.toFloat()) Pair(0f, width.toFloat())
} }
textLine.textChars.add( textLine.textChars.add(
TextChar(charData = src, start = x + start, end = x + end, isImage = true) TextColumn(charData = src, start = x + start, end = x + end, style = 1)
) )
textPages.last().textLines.add(textLine) textPages.last().textLines.add(textLine)
} }
@ -276,9 +288,8 @@ object ChapterProvider {
srcList: LinkedList<String>? = null srcList: LinkedList<String>? = null
): Pair<Int, Float> { ): Pair<Int, Float> {
var absStartX = x var absStartX = x
val layout = if (ReadBookConfig.useZhLayout) { val layout = if (ReadBookConfig.useZhLayout) ZhLayout(text, textPaint, visibleWidth)
ZhLayout(text, textPaint, visibleWidth) else StaticLayout(
} else StaticLayout(
text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true
) )
var durY = when { var durY = when {
@ -398,10 +409,14 @@ object ChapterProvider {
} }
val bodyIndent = ReadBookConfig.paragraphIndent val bodyIndent = ReadBookConfig.paragraphIndent
val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
bodyIndent.toStringArray().forEach { char -> for (char in bodyIndent.toStringArray()) {
val x1 = x + icw val x1 = x + icw
textLine.textChars.add( textLine.textChars.add(
TextChar(charData = char, start = absStartX + x, end = absStartX + x1) TextColumn(
charData = char,
start = absStartX + x,
end = absStartX + x1
)
) )
x = x1 x = x1
} }
@ -436,7 +451,7 @@ object ChapterProvider {
words.forEachIndexed { index, char -> words.forEachIndexed { index, char ->
val cw = StaticLayout.getDesiredWidth(char, textPaint) val cw = StaticLayout.getDesiredWidth(char, textPaint)
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(book, absStartX, textLine, char, x, x1, srcList) addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1 x = x1
} }
exceed(absStartX, textLine, words) exceed(absStartX, textLine, words)
@ -455,10 +470,10 @@ object ChapterProvider {
srcList: LinkedList<String>? srcList: LinkedList<String>?
) { ) {
var x = startX var x = startX
words.forEach { char -> words.forEachIndexed { index, char ->
val cw = StaticLayout.getDesiredWidth(char, textPaint) val cw = StaticLayout.getDesiredWidth(char, textPaint)
val x1 = x + cw val x1 = x + cw
addCharToLine(book, absStartX, textLine, char, x, x1, srcList) addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
x = x1 x = x1
} }
exceed(absStartX, textLine, words) exceed(absStartX, textLine, words)
@ -474,25 +489,27 @@ object ChapterProvider {
char: String, char: String,
xStart: Float, xStart: Float,
xEnd: Float, xEnd: Float,
isLineEnd: Boolean,
srcList: LinkedList<String>? srcList: LinkedList<String>?
) { ) {
if (srcList != null && char == srcReplaceChar) { if (srcList != null && char == srcReplaceChar) {
val src = srcList.removeFirst() val src = srcList.removeFirst()
ImageProvider.cacheImage(book, src, ReadBook.bookSource) ImageProvider.cacheImage(book, src, ReadBook.bookSource)
textLine.textChars.add( textLine.textChars.add(
TextChar( TextColumn(
charData = src, charData = src,
start = absStartX + xStart, start = absStartX + xStart,
end = absStartX + xEnd, end = absStartX + xEnd,
isImage = true style = 1
) )
) )
} else { } else {
textLine.textChars.add( textLine.textChars.add(
TextChar( TextColumn(
charData = char, charData = char,
start = absStartX + xStart, start = absStartX + xStart,
end = absStartX + xEnd end = absStartX + xEnd,
style = if (isLineEnd && char == "\uD83D\uDCAC") 2 else 0
) )
) )
} }
@ -612,12 +629,12 @@ object ChapterProvider {
* 更新绘制尺寸 * 更新绘制尺寸
*/ */
fun upLayout() { fun upLayout() {
when(AppConfig.doublePageHorizontal){ when (AppConfig.doublePageHorizontal) {
"0" -> doublePage = false "0" -> doublePage = false
"1" -> doublePage = true "1" -> doublePage = true
"2" -> { "2" -> {
doublePage = (viewWidth > viewHeight) doublePage = (viewWidth > viewHeight)
&& ReadBook.pageAnim() != 3 && ReadBook.pageAnim() != 3
} }
} }

@ -41,10 +41,11 @@ class BookSourceEditActivity :
private val adapter by lazy { BookSourceEditAdapter() } private val adapter by lazy { BookSourceEditAdapter() }
private val sourceEntities: ArrayList<EditEntity> = ArrayList() private val sourceEntities: ArrayList<EditEntity> = ArrayList()
private val searchEntities: ArrayList<EditEntity> = ArrayList() private val searchEntities: ArrayList<EditEntity> = ArrayList()
private val findEntities: ArrayList<EditEntity> = ArrayList() private val exploreEntities: ArrayList<EditEntity> = ArrayList()
private val infoEntities: ArrayList<EditEntity> = ArrayList() private val infoEntities: ArrayList<EditEntity> = ArrayList()
private val tocEntities: ArrayList<EditEntity> = ArrayList() private val tocEntities: ArrayList<EditEntity> = ArrayList()
private val contentEntities: ArrayList<EditEntity> = ArrayList() private val contentEntities: ArrayList<EditEntity> = ArrayList()
private val reviewEntities: ArrayList<EditEntity> = ArrayList()
private val qrCodeResult = registerForActivityResult(QrCodeResult()) { private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult it ?: return@registerForActivityResult
viewModel.importSource(it) { source -> viewModel.importSource(it) { source ->
@ -180,10 +181,11 @@ class BookSourceEditActivity :
private fun setEditEntities(tabPosition: Int?) { private fun setEditEntities(tabPosition: Int?) {
when (tabPosition) { when (tabPosition) {
1 -> adapter.editEntities = searchEntities 1 -> adapter.editEntities = searchEntities
2 -> adapter.editEntities = findEntities 2 -> adapter.editEntities = exploreEntities
3 -> adapter.editEntities = infoEntities 3 -> adapter.editEntities = infoEntities
4 -> adapter.editEntities = tocEntities 4 -> adapter.editEntities = tocEntities
5 -> adapter.editEntities = contentEntities 5 -> adapter.editEntities = contentEntities
6 -> adapter.editEntities = reviewEntities
else -> adapter.editEntities = sourceEntities else -> adapter.editEntities = sourceEntities
} }
binding.recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
@ -192,8 +194,9 @@ class BookSourceEditActivity :
private fun upSourceView(source: BookSource? = viewModel.bookSource) { private fun upSourceView(source: BookSource? = viewModel.bookSource) {
source?.let { source?.let {
binding.cbIsEnable.isChecked = it.enabled binding.cbIsEnable.isChecked = it.enabled
binding.cbIsEnableFind.isChecked = it.enabledExplore binding.cbIsEnableExplore.isChecked = it.enabledExplore
binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
binding.cbIsEnableReview.isChecked = it.enabledReview ?: false
binding.spType.setSelection( binding.spType.setSelection(
when (it.bookSourceType) { when (it.bookSourceType) {
BookType.file -> 3 BookType.file -> 3
@ -203,7 +206,7 @@ class BookSourceEditActivity :
} }
) )
} }
//基本信息 // 基本信息
sourceEntities.clear() sourceEntities.clear()
sourceEntities.apply { sourceEntities.apply {
add(EditEntity("bookSourceUrl", source?.bookSourceUrl, R.string.source_url)) add(EditEntity("bookSourceUrl", source?.bookSourceUrl, R.string.source_url))
@ -218,7 +221,7 @@ class BookSourceEditActivity :
add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment)) add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment))
add(EditEntity("concurrentRate", source?.concurrentRate, R.string.concurrent_rate)) add(EditEntity("concurrentRate", source?.concurrentRate, R.string.concurrent_rate))
} }
//搜索 // 搜索
val sr = source?.getSearchRule() val sr = source?.getSearchRule()
searchEntities.clear() searchEntities.clear()
searchEntities.apply { searchEntities.apply {
@ -234,10 +237,10 @@ class BookSourceEditActivity :
add(EditEntity("coverUrl", sr?.coverUrl, R.string.rule_cover_url)) add(EditEntity("coverUrl", sr?.coverUrl, R.string.rule_cover_url))
add(EditEntity("bookUrl", sr?.bookUrl, R.string.r_book_url)) add(EditEntity("bookUrl", sr?.bookUrl, R.string.r_book_url))
} }
//发现 // 发现
val er = source?.getExploreRule() val er = source?.getExploreRule()
findEntities.clear() exploreEntities.clear()
findEntities.apply { exploreEntities.apply {
add(EditEntity("exploreUrl", source?.exploreUrl, R.string.r_find_url)) add(EditEntity("exploreUrl", source?.exploreUrl, R.string.r_find_url))
add(EditEntity("bookList", er?.bookList, R.string.r_book_list)) add(EditEntity("bookList", er?.bookList, R.string.r_book_list))
add(EditEntity("name", er?.name, R.string.r_book_name)) add(EditEntity("name", er?.name, R.string.r_book_name))
@ -249,7 +252,7 @@ class BookSourceEditActivity :
add(EditEntity("coverUrl", er?.coverUrl, R.string.rule_cover_url)) add(EditEntity("coverUrl", er?.coverUrl, R.string.rule_cover_url))
add(EditEntity("bookUrl", er?.bookUrl, R.string.r_book_url)) add(EditEntity("bookUrl", er?.bookUrl, R.string.r_book_url))
} }
//详情页 // 详情页
val ir = source?.getBookInfoRule() val ir = source?.getBookInfoRule()
infoEntities.clear() infoEntities.clear()
infoEntities.apply { infoEntities.apply {
@ -265,7 +268,7 @@ class BookSourceEditActivity :
add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name)) add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule)) add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
} }
//目录页 // 目录页
val tr = source?.getTocRule() val tr = source?.getTocRule()
tocEntities.clear() tocEntities.clear()
tocEntities.apply { tocEntities.apply {
@ -279,7 +282,7 @@ class BookSourceEditActivity :
add(EditEntity("isPay", tr?.isPay, R.string.rule_is_pay)) add(EditEntity("isPay", tr?.isPay, R.string.rule_is_pay))
add(EditEntity("nextTocUrl", tr?.nextTocUrl, R.string.rule_next_toc_url)) add(EditEntity("nextTocUrl", tr?.nextTocUrl, R.string.rule_next_toc_url))
} }
//正文页 // 正文页
val cr = source?.getContentRule() val cr = source?.getContentRule()
contentEntities.clear() contentEntities.clear()
contentEntities.apply { contentEntities.apply {
@ -291,6 +294,21 @@ class BookSourceEditActivity :
add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style)) add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style))
add(EditEntity("payAction", cr?.payAction, R.string.rule_pay_action)) add(EditEntity("payAction", cr?.payAction, R.string.rule_pay_action))
} }
// 段评
val rr = source?.getReviewRule()
reviewEntities.clear()
reviewEntities.apply {
add(EditEntity("reviewUrl", rr?.reviewUrl, R.string.rule_review_url))
add(EditEntity("avatarRule", rr?.avatarRule, R.string.rule_avatar))
add(EditEntity("contentRule", rr?.contentRule, R.string.rule_review_content))
add(EditEntity("postTimeRule", rr?.postTimeRule, R.string.rule_post_time))
add(EditEntity("reviewQuoteUrl", rr?.reviewQuoteUrl, R.string.rule_review_quote))
add(EditEntity("voteUpUrl", rr?.voteUpUrl, R.string.review_vote_up))
add(EditEntity("voteDownUrl", rr?.voteDownUrl, R.string.review_vote_down))
add(EditEntity("postReviewUrl", rr?.postReviewUrl, R.string.post_review_url))
add(EditEntity("postQuoteUrl", rr?.postQuoteUrl, R.string.post_quote_url))
add(EditEntity("deleteUrl", rr?.deleteUrl, R.string.delete_review_url))
}
binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0)) binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
setEditEntities(0) setEditEntities(0)
} }
@ -298,8 +316,9 @@ class BookSourceEditActivity :
private fun getSource(): BookSource { private fun getSource(): BookSource {
val source = viewModel.bookSource?.copy() ?: BookSource() val source = viewModel.bookSource?.copy() ?: BookSource()
source.enabled = binding.cbIsEnable.isChecked source.enabled = binding.cbIsEnable.isChecked
source.enabledExplore = binding.cbIsEnableFind.isChecked source.enabledExplore = binding.cbIsEnableExplore.isChecked
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
source.enabledReview = binding.cbIsEnableReview.isChecked
source.bookSourceType = when (binding.spType.selectedItemPosition) { source.bookSourceType = when (binding.spType.selectedItemPosition) {
3 -> BookType.file 3 -> BookType.file
2 -> BookType.image 2 -> BookType.image
@ -311,6 +330,7 @@ class BookSourceEditActivity :
val bookInfoRule = BookInfoRule() val bookInfoRule = BookInfoRule()
val tocRule = TocRule() val tocRule = TocRule()
val contentRule = ContentRule() val contentRule = ContentRule()
val reviewRule = ReviewRule()
sourceEntities.forEach { sourceEntities.forEach {
when (it.key) { when (it.key) {
"bookSourceUrl" -> source.bookSourceUrl = it.value ?: "" "bookSourceUrl" -> source.bookSourceUrl = it.value ?: ""
@ -351,7 +371,7 @@ class BookSourceEditActivity :
viewModel.ruleComplete(it.value, searchRule.bookList, 2) viewModel.ruleComplete(it.value, searchRule.bookList, 2)
} }
} }
findEntities.forEach { exploreEntities.forEach {
when (it.key) { when (it.key) {
"exploreUrl" -> source.exploreUrl = it.value "exploreUrl" -> source.exploreUrl = it.value
"bookList" -> exploreRule.bookList = it.value "bookList" -> exploreRule.bookList = it.value
@ -429,11 +449,30 @@ class BookSourceEditActivity :
"payAction" -> contentRule.payAction = it.value "payAction" -> contentRule.payAction = it.value
} }
} }
reviewEntities.forEach {
when (it.key) {
"reviewUrl" -> reviewRule.reviewUrl = it.value
"avatarRule" -> reviewRule.avatarRule =
viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 3)
"contentRule" -> reviewRule.contentRule =
viewModel.ruleComplete(it.value, reviewRule.reviewUrl)
"postTimeRule" -> reviewRule.postTimeRule =
viewModel.ruleComplete(it.value, reviewRule.reviewUrl)
"reviewQuoteUrl" -> reviewRule.reviewQuoteUrl =
viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 2)
"voteUpUrl" -> reviewRule.voteUpUrl = it.value
"voteDownUrl" -> reviewRule.voteDownUrl = it.value
"postReviewUrl" -> reviewRule.postReviewUrl = it.value
"postQuoteUrl" -> reviewRule.postQuoteUrl = it.value
"deleteUrl" -> reviewRule.deleteUrl =it.value
}
}
source.ruleSearch = searchRule source.ruleSearch = searchRule
source.ruleExplore = exploreRule source.ruleExplore = exploreRule
source.ruleBookInfo = bookInfoRule source.ruleBookInfo = bookInfoRule
source.ruleToc = tocRule source.ruleToc = tocRule
source.ruleContent = contentRule source.ruleContent = contentRule
source.ruleReview = reviewRule
return source return source
} }

@ -27,7 +27,7 @@
android:text="@string/is_enable" /> android:text="@string/is_enable" />
<io.legado.app.lib.theme.view.ThemeCheckBox <io.legado.app.lib.theme.view.ThemeCheckBox
android:id="@+id/cb_is_enable_find" android:id="@+id/cb_is_enable_explore"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:checked="true"
@ -40,18 +40,33 @@
android:checked="true" android:checked="true"
android:text="@string/auto_save_cookie" /> android:text="@string/auto_save_cookie" />
<TextView <io.legado.app.lib.theme.view.ThemeCheckBox
android:id="@+id/cb_is_enable_review"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true"
android:text="@string/review" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:height="30dp"
android:gravity="center"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="@string/book_type" android:text="@string/book_type"
android:layout_marginLeft="12dp" android:layout_marginLeft="8dp"
android:layout_marginRight="3dp" android:layout_marginRight="3dp"
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
<androidx.appcompat.widget.AppCompatSpinner <androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/sp_type" android:id="@+id/sp_type"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:entries="@array/book_type" android:entries="@array/book_type"
app:theme="@style/Spinner" /> app:theme="@style/Spinner" />
@ -94,6 +109,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/source_tab_content" /> android:text="@string/source_tab_content" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/review" />
</com.google.android.material.tabs.TabLayout> </com.google.android.material.tabs.TabLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView

@ -84,6 +84,13 @@
android:checked="false" android:checked="false"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_enable_review"
android:title="@string/review"
android:checkable="true"
android:checked="false"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_image_style" android:id="@+id/menu_image_style"
android:title="@string/image_style" android:title="@string/image_style"

@ -1010,6 +1010,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">Review</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">show read title addition area</string> <string name="show_read_title_addition">show read title addition area</string>

@ -1013,6 +1013,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">Review</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">show read title addition area</string> <string name="show_read_title_addition">show read title addition area</string>

@ -1013,6 +1013,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">Review</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">show read title addition area</string> <string name="show_read_title_addition">show read title addition area</string>

@ -1010,6 +1010,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">段评</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">展示顶部工具栏附加区域</string> <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

@ -1012,6 +1012,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">段评</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">展示顶部工具栏附加区域</string> <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

@ -1012,6 +1012,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">段评</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">展示顶部工具栏附加区域</string> <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

@ -1,25 +1,25 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation"> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="pk_bookshelf_px">bookshelf_px</string> <string name="pk_bookshelf_px" translatable="false">bookshelf_px</string>
<string name="legado_gzh">开源阅读</string> <string name="legado_gzh" translatable="false">开源阅读</string>
<string name="email">kunfei.ge@gmail.com</string> <string name="email" translatable="false">kunfei.ge@gmail.com</string>
<string name="source_rule_url">https://alanskycn.gitee.io/teachme/</string> <string name="source_rule_url" translatable="false">https://alanskycn.gitee.io/teachme/</string>
<string name="this_github_url">https://github.com/gedoor/legado</string> <string name="this_github_url" translatable="false">https://github.com/gedoor/legado</string>
<string name="contributors_url">https://github.com/gedoor/legado/graphs/contributors</string> <string name="contributors_url" translatable="false">https://github.com/gedoor/legado/graphs/contributors</string>
<string name="home_page_url">https://gedoor.github.io</string> <string name="home_page_url" translatable="false">https://gedoor.github.io</string>
<string name="license_url">https://github.com/gedoor/legado/blob/master/LICENSE</string> <string name="license_url" translatable="false">https://github.com/gedoor/legado/blob/master/LICENSE</string>
<string name="latest_release_url">https://github.com/gedoor/legado/releases/latest</string> <string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string>
<string name="latest_release_api">https://api.github.com/repos/gedoor/legado/releases/latest</string> <string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string>
<string name="tg_url">https://t.me/legado_channels</string> <string name="tg_url" translatable="false">https://t.me/legado_channels</string>
<string name="discord_url">https://discord.gg/qDE52P5xGW</string> <string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string>
<string name="http_ip">http://%1$s:%2$d</string> <string name="http_ip" translatable="false">http://%1$s:%2$d</string>
<string name="git_hub">GitHub</string> <string name="git_hub" translatable="false">GitHub</string>
<string name="diy_edit_source_group_title">【%s】</string> <string name="diy_edit_source_group_title" translatable="false">【%s】</string>
<string name="vip_title">🔒%s</string> <string name="vip_title" translatable="false">🔒%s</string>
<string name="payed_title">🔓%s</string> <string name="payed_title" translatable="false">🔓%s</string>
<string name="separator"></string> <string name="separator" translatable="false"></string>
</resources> </resources>

@ -1013,6 +1013,17 @@
<string name="check_selected_interval">选中所选区间</string> <string name="check_selected_interval">选中所选区间</string>
<string name="show_add_to_shelf_alert_title">返回时提示放入书架</string> <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
<string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string> <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
<string name="review">Review</string>
<string name="rule_avatar">段评发布者头像(avatarRule)</string>
<string name="rule_review_url">段评URL(reviewUrl)</string>
<string name="rule_review_content">段评内容(contentRule)</string>
<string name="rule_post_time">段评发布时间(postTimeRule)</string>
<string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
<string name="review_vote_down">点踩URL(voteUpUrl)</string>
<string name="review_vote_up">点赞URL(voteDownUrl)</string>
<string name="post_review_url">发送回复URL(postReviewUrl)</string>
<string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
<string name="delete_review_url">删除段评URL(deleteUrl)</string>
<string name="tag_explore_enabled">标志:发现已启用</string> <string name="tag_explore_enabled">标志:发现已启用</string>
<string name="tag_explore_disabled">标志:发现已禁用</string> <string name="tag_explore_disabled">标志:发现已禁用</string>
<string name="show_read_title_addition">show read title addition area</string> <string name="show_read_title_addition">show read title addition area</string>

Loading…
Cancel
Save