[优化] 同步上游更新 (@821938089)

This commit is contained in:
HapeLee
2025-12-20 03:33:17 +08:00
parent 9e8ab55e02
commit 060ec9ce1a
44 changed files with 373 additions and 205 deletions
+4
View File
@@ -43,6 +43,8 @@ import io.legado.app.help.LifecycleHelp
import io.legado.app.help.RuleBigDataHelp
import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.help.config.ThemeConfig.applyDayNightInit
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.http.Cronet
@@ -152,6 +154,8 @@ class App : Application() {
RuleBigDataHelp.clearInvalid()
BookHelp.clearInvalidCache()
Backup.clearCache()
ReadBookConfig.clearBgAndCache()
ThemeConfig.clearBg()
//初始化简繁转换引擎
when (AppConfig.chineseConverterType) {
1 -> {
@@ -206,7 +206,7 @@ object PreferKey {
const val notificationsPost = "notificationsPost"
const val ignoreBatteryPermission = "ignoreBatteryPermission"
const val paddingDisplayCutouts = "paddingDisplayCutouts"
const val autoCheckNewBackup = "autoCheckNewBackup"
const val delayBookLoadEnable = "delayBookLoadEnable"
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
@@ -1,7 +1,7 @@
package io.legado.app.data.entities.rule
data class RowUi(
var name: String,
var name: String = "",
var type: String = "text",
var action: String? = null,
var style: FlexChildStyle? = null
@@ -322,7 +322,7 @@ interface JsExtensions : JsEncodeUtils {
fun downloadFile(url: String): String {
rhinoContext.ensureActive()
val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
val type = UrlUtil.getSuffix(url, analyzeUrl.type)
val type = analyzeUrl.type ?: UrlUtil.getSuffix(url)
val path = FileUtils.getPath(
File(FileUtils.getCachePath()),
"${MD5Utils.md5Encode16(url)}.${type}"
@@ -20,13 +20,13 @@ object ContentHelp {
var p = content1
.replace(""".toRegex(), "")
.replace("[:]['\"‘”“]+".toRegex(), ":“")
.replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "\n")
.replace("[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "\n")
.split("\n(\\s*)".toRegex()).toTypedArray()
//初始化StringBuilder的长度,在原content的长度基础上做冗余
var buffer = StringBuilder((content1.length * 1.15).toInt())
// 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。
buffer.append(" ")
buffer.append(" ")
if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) {
// 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), ""))
@@ -34,7 +34,12 @@ object ContentHelp {
//如果原文存在分段错误,需要把段落重新黏合
for (i in 1 until p.size) {
if (match(MARK_SENTENCES_END, buffer[buffer.length - 1])) buffer.append("\n")
if (match(MARK_SENTENCES_END, buffer.last())
|| (match(MARK_QUOTATION_RIGHT, buffer.last())
&& match(MARK_SENTENCES_END, buffer[buffer.lastIndex - 1]))
) {
buffer.append("\n")
}
// 段落开头以外的地方不应该有空格
// 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
buffer.append(p[i].replace("[\u3000\\s]".toRegex(), ""))
@@ -44,7 +49,7 @@ object ContentHelp {
// ”。“处理为”。\n“。不考虑“?” “!”的情况。
// ”。xxx处理为 ”。\n xxx
p = buffer.toString()
.replace("[\"”“]+[\\s]*[\"”“]+".toRegex(), "\n")
.replace("[\"”“]+\\s*[\"”“]+".toRegex(), "\n")
.replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "$1\n")
.replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "$1\n$2")
.replace(
@@ -60,7 +65,7 @@ object ContentHelp {
buffer = reduceLength(buffer)
content1 = (buffer.toString() // 处理章节头部空格和换行
.replaceFirst("^\\s+".toRegex(), "")
.replace("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "\n")
.replace("\\s*[\"”“]+\\s*[\"”“][\\s\"”“]*".toRegex(), "\n")
.replace("[:][”“\"\\s]+".toRegex(), ":“")
.replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3")
.replace("\n(\\s*)".toRegex(), "\n"))
@@ -113,13 +118,13 @@ object ContentHelp {
if (match(MARK_QUOTATION, str[0])) {
val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1
if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
return "${str.substring(0, i)}\n${str.substring(i)}"
return "${str.take(i)}\n${str.substring(i)}"
}
} else if (match(MARK_QUOTATION, str[length - 1])) {
val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false)
if (i > 1) {
if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
return "${str.substring(0, i)}\n${str.substring(i)}"
return "${str.take(i)}\n${str.substring(i)}"
}
}
}
@@ -142,8 +147,8 @@ object ContentHelp {
tigger: Int
): ArrayList<Int> {
val result = ArrayList<Int>()
val arrayEnd = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length - 2, true)
val arrayMid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length - 2, true)
val arrayEnd = seekIndexes(str, MARK_SENTENCES_END_P, 0, str.length - 2, true)
val arrayMid = seekIndexes(str, MARK_SENTENCES_MID, 0, str.length - 2, true)
if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result
var j = 0
var i = min
@@ -347,7 +352,7 @@ object ContentHelp {
var j = 0
var progress = 0
var nextLine = -1
if (insN.size > 0) nextLine = insN[j]
if (insN.isNotEmpty()) nextLine = insN[j]
var gain = 3
var min = 0
var trigger = 2
@@ -425,24 +430,24 @@ object ContentHelp {
var j = 0
var progress = 0
var nextLine = -1
if (insN.size > 0) nextLine = insN[j]
if (insN.isNotEmpty()) nextLine = insN[j]
for (i in arrayQuote.indices) {
val qutoe = arrayQuote[i]
val quote = arrayQuote[i]
// 把引号前的换行符与内容相间插入
while (j < insN.size) {
// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
if (nextLine >= qutoe) break
if (nextLine >= quote) break
nextLine = insN[j]
buffer.append(string, progress, nextLine + 1)
buffer.append('\n')
progress = nextLine + 1
j++
}
if (progress < qutoe) {
buffer.append(string, progress, qutoe + 1)
progress = qutoe + 1
if (progress < quote) {
buffer.append(string, progress, quote + 1)
progress = quote + 1
}
if (insQuote[i] && buffer.length > 2) {
if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert(
@@ -504,7 +509,7 @@ object ContentHelp {
* @param inOrder 是否按照从前向后的顺序匹配
* @return 返回距离构成的ArrayList<Int>
*/
private fun seekIndexs(
private fun seekIndexes(
str: String,
key: String,
from: Int,
@@ -514,14 +519,18 @@ object ContentHelp {
val list = ArrayList<Int>()
if (str.length - from < 1) return list
var i = 0
if (from > i) i = from
if (from > 0) i = from
var t = str.length
if (to > 0) t = min(t, to)
var c: Char
while (i < t) {
c = if (inOrder) str[i] else str[str.length - i - 1]
if (key.indexOf(c) != -1) {
list.add(i)
if (list.isNotEmpty() && i - list.last() == 1) {
list[list.lastIndex] = i
} else {
list.add(i)
}
}
i++
}
@@ -567,7 +576,7 @@ object ContentHelp {
private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int {
if (str.length - from < 1) return -1
var i = 0
if (from > i) i = from
if (from > 0) i = from
var t = str.length
if (to > 0) t = min(t, to)
var c: Char
@@ -609,6 +618,7 @@ object ContentHelp {
// 引号
private const val MARK_QUOTATION = "\"“”"
private const val MARK_QUOTATION_RIGHT = "\""
private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex()
// 限制字典的长度
@@ -580,6 +580,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val onlyLatestBackup get() = appCtx.getPrefBoolean(PreferKey.onlyLatestBackup, true)
val autoCheckNewBackup get() = appCtx.getPrefBoolean(PreferKey.autoCheckNewBackup, true)
val defaultHomePage get() = appCtx.getPrefString(PreferKey.defaultHomePage, "bookshelf")
val updateToVariant get() = appCtx.getPrefString(PreferKey.updateToVariant, "official_version")
@@ -33,6 +33,7 @@ import splitties.init.appCtx
import java.io.File
import kotlin.String
import androidx.core.graphics.drawable.toDrawable
import io.legado.app.utils.FileDoc
import io.legado.app.utils.putPrefBoolean
/**
@@ -164,6 +165,25 @@ object ReadBookConfig {
return false
}
fun clearBgAndCache() {
val bgs = hashSetOf<String>()
configList.forEach { config ->
repeat(3) {
config.getBgPath(it)?.let { path ->
bgs.add(path)
}
}
}
appCtx.externalFiles.getFile("bg").listFiles()?.forEach {
if (!bgs.contains(it.absolutePath)) {
it.delete()
}
}
FileUtils.delete(appCtx.externalCache.getFile("readConfig"))
val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
FileUtils.delete(configZipPath)
}
private fun resetAll() {
DefaultData.readConfigs.let {
configList.clear()
@@ -541,13 +561,18 @@ object ReadBookConfig {
val configFile = configDir.getFile(configFileName)
val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
if (config.textFont.isNotEmpty()) {
val fontName = FileUtils.getName(config.textFont)
val fontName = config.textFont
val fontPath =
FileUtils.getPath(appCtx.externalFiles, "font", fontName)
if (!FileUtils.exist(fontPath)) {
configDir.getFile(fontName).copyTo(File(fontPath))
val fontFile = configDir.getFile(fontName)
if (fontFile.exists()) {
if (!FileUtils.exist(fontPath)) {
fontFile.copyTo(File(fontPath))
}
config.textFont = fontPath
} else {
config.textFont = ""
}
config.textFont = fontPath
}
if (config.bgType == 2) {
val bgName = FileUtils.getName(config.bgStr)
@@ -696,6 +721,30 @@ object ReadBookConfig {
@Transient
private var initColorInt = false
fun getBgPath(bgIndex: Int): String? {
val bgType = when (bgIndex) {
0 -> bgType
1 -> bgTypeNight
2 -> bgTypeEInk
else -> error("unknown bgIndex: $bgIndex")
}
if (bgType != 2) {
return null
}
val bgStr = when (bgIndex) {
0 -> bgStr
1 -> bgStrNight
2 -> bgStrEInk
else -> error("unknown bgIndex: $bgIndex")
}
val path = if (bgStr.contains(File.separator)) {
bgStr
} else {
FileUtils.getPath(appCtx.externalFiles, "bg", bgStr)
}
return path
}
private fun initColorInt() {
textColorIntEInk = textColorEInk.toColorInt()
textColorIntNight = textColorNight.toColorInt()
@@ -16,9 +16,11 @@ import io.legado.app.model.BookCover
import io.legado.app.utils.BitmapUtils
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
import io.legado.app.utils.externalFiles
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getCompatColor
import io.legado.app.utils.getFile
import io.legado.app.utils.getPrefInt
import io.legado.app.utils.getPrefString
import io.legado.app.utils.hexString
@@ -236,9 +238,20 @@ object ThemeConfig {
/**
* 更新主题
*/
// fun applyTheme(context: Context) = with(context) {
//
// }
fun clearBg() {
val bgImagePath = appCtx.getPrefString(PreferKey.bgImage)
appCtx.externalFiles.getFile(PreferKey.bgImage).listFiles()?.forEach {
if (it.absolutePath != bgImagePath) {
it.delete()
}
}
val bgImageNPath = appCtx.getPrefString(PreferKey.bgImageN)
appCtx.externalFiles.getFile(PreferKey.bgImageN).listFiles()?.forEach {
if (it.absolutePath != bgImageNPath) {
it.delete()
}
}
}
@Keep
data class Config(
@@ -46,9 +46,7 @@ suspend fun OkHttpClient.newCallResponseBody(
retry: Int = 0,
builder: Request.Builder.() -> Unit
): ResponseBody {
return newCallResponse(retry, builder).let {
it.body ?: throw IOException(it.message)
}
return newCallResponse(retry, builder).body
}
suspend fun OkHttpClient.newCallStrResponse(
@@ -56,7 +54,7 @@ suspend fun OkHttpClient.newCallStrResponse(
builder: Request.Builder.() -> Unit
): StrResponse {
return newCallResponse(retry, builder).let {
StrResponse(it, it.body?.text() ?: it.message)
StrResponse(it, it.body.text())
}
}
@@ -142,6 +140,7 @@ fun Request.Builder.postForm(encodedForm: String) {
post(encodedForm.toRequestBody(formContentType))
}
@Suppress("unused")
fun Request.Builder.postForm(form: Map<String, String>, encoded: Boolean = false) {
val formBody = FormBody.Builder()
form.forEach {
@@ -14,9 +14,11 @@ import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.help.coroutine.Coroutine
import io.legado.app.model.BookCover
import io.legado.app.utils.*
import io.legado.app.utils.compress.ZipUtils
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -29,6 +31,8 @@ import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.coroutines.coroutineContext
import androidx.core.content.edit
import androidx.core.net.toUri
/**
* 备份
@@ -65,6 +69,7 @@ object Backup {
ReadBookConfig.configFileName,
ReadBookConfig.shareConfigFileName,
ThemeConfig.configFileName,
BookCover.configFileName,
"config.xml"
)
}
@@ -139,7 +144,7 @@ object Backup {
.writeText(it)
}
}
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
GSON.toJson(ReadBookConfig.configList).let {
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.configFileName)
.writeText(it)
@@ -156,31 +161,35 @@ object Backup {
FileUtils.createFileIfNotExist(backupPath + File.separator + DirectLinkUpload.ruleFileName)
.writeText(GSON.toJson(it))
}
coroutineContext.ensureActive()
BookCover.getConfig()?.let {
FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
.writeText(GSON.toJson(it))
}
currentCoroutineContext().ensureActive()
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
val edit = sp.edit()
appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
edit.putString(key, aes.runCatching {
encryptBase64(value.toString())
}.getOrDefault(value.toString()))
}
sp.edit(commit = true) {
appCtx.defaultSharedPreferences.all.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
putString(key, aes.runCatching {
encryptBase64(value.toString())
}.getOrDefault(value.toString()))
}
else -> when (value) {
is Int -> edit.putInt(key, value)
is Boolean -> edit.putBoolean(key, value)
is Long -> edit.putLong(key, value)
is Float -> edit.putFloat(key, value)
is String -> edit.putString(key, value)
else -> when (value) {
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is String -> putString(key, value)
}
}
}
}
}
edit.commit()
}
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
val zipFileName = getNowZipFileName()
val paths = arrayListOf(*backupFileNames)
for (i in 0 until paths.size) {
@@ -200,7 +209,7 @@ object Backup {
}
path.isContentScheme() -> {
copyBackup(context, Uri.parse(path), backupFileName)
copyBackup(context, path.toUri(), backupFileName)
}
else -> {
@@ -215,7 +224,7 @@ object Backup {
}
FileUtils.delete(backupPath)
FileUtils.delete(zipFilePath)
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
ReadBookConfig.getAllPicBgStr().map {
if (it.contains(File.separator)) {
File(it)
@@ -228,7 +237,7 @@ object Backup {
}
private suspend fun writeListToJson(list: List<Any>, fileName: String, path: String) {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
withContext(IO) {
if (list.isNotEmpty()) {
LogUtils.d(TAG, "阅读备份 $fileName 列表大小 ${list.size}")
@@ -33,6 +33,7 @@ import io.legado.app.help.book.upType
import io.legado.app.help.config.LocalConfig
import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ThemeConfig
import io.legado.app.model.BookCover
import io.legado.app.model.localBook.LocalBook
import io.legado.app.utils.ACache
import io.legado.app.utils.FileUtils
@@ -212,6 +213,14 @@ object Restore {
}?.onFailure {
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
}
File(path, BookCover.configFileName).takeIf {
it.exists()
}?.runCatching {
val json = readText()
BookCover.saveCoverRule(json)
}?.onFailure {
AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it)
}
if (!BackupConfig.ignoreReadConfig) {
//恢复阅读界面配置
File(path, ReadBookConfig.configFileName).takeIf {
@@ -16,6 +16,7 @@ import io.legado.app.utils.findNSPrefix
import io.legado.app.utils.printOnDebug
import io.legado.app.utils.toRequestBody
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
@@ -257,7 +258,7 @@ open class WebDav(
method("PROPFIND", requestBody)
}.use { it.isSuccessful }
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
}.getOrDefault(false)
}
@@ -273,7 +274,7 @@ open class WebDav(
method("PROPFIND", requestBody)
}.use { it.code != 401 }
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
}.getOrDefault(true)
}
@@ -294,7 +295,7 @@ open class WebDav(
}
}
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
AppLog.put("WebDav创建目录失败\n${it.localizedMessage}", it)
}.isSuccess
}
@@ -351,7 +352,7 @@ open class WebDav(
}
}
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
}
@@ -372,7 +373,7 @@ open class WebDav(
}
}
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
}
@@ -393,7 +394,7 @@ open class WebDav(
}
}
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
AppLog.put("WebDav上传失败\n${it.localizedMessage}", it)
throw WebDavException("WebDav上传失败\n${it.localizedMessage}")
}
@@ -424,7 +425,7 @@ open class WebDav(
checkResult(it)
}
}.onFailure {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
AppLog.put("WebDav删除失败\n${it.localizedMessage}", it)
}.isSuccess
}
@@ -142,6 +142,11 @@ object AudioPlay : CoroutineScope by MainScope() {
removeLoading(index)
return
}
if (chapter.isVolume) {
skipTo(index + 1)
removeLoading(index)
return
}
upLoading(true)
WebBook.getContent(this, bookSource, book, chapter)
.onSuccess { content ->
@@ -208,7 +213,8 @@ object AudioPlay : CoroutineScope by MainScope() {
val book = book ?: return
durChapter = appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)
durAudioSize = durChapter?.end?.toInt() ?: 0
postEvent(EventBus.AUDIO_SUB_TITLE, durChapter?.title ?: appCtx.getString(R.string.data_loading))
val title = durChapter?.title ?: appCtx.getString(R.string.data_loading)
postEvent(EventBus.AUDIO_SUB_TITLE, title)
postEvent(EventBus.AUDIO_SIZE, durAudioSize)
postEvent(EventBus.AUDIO_PROGRESS, durChapterPos)
}
@@ -260,11 +266,13 @@ object AudioPlay : CoroutineScope by MainScope() {
fun skipTo(index: Int) {
Coroutine.async {
stopPlay()
durChapterIndex = index
durChapterPos = 0
durPlayUrl = ""
saveRead()
loadPlayUrl()
if (index in 0..<simulatedChapterSize) {
durChapterIndex = index
durChapterPos = 0
durPlayUrl = ""
saveRead()
loadPlayUrl()
}
}
}
@@ -46,6 +46,7 @@ import java.io.File
object BookCover {
private const val coverRuleConfigKey = "legadoCoverRuleConfig"
const val configFileName = "coverRule.json"
var drawBookName = true
private set
var drawBookAuthor = true
@@ -239,9 +240,12 @@ object BookCover {
}
fun getCoverRule(): CoverRule {
return getConfig() ?: DefaultData.coverRule
}
fun getConfig(): CoverRule? {
return GSON.fromJsonObject<CoverRule>(CacheManager.get(coverRuleConfigKey))
.getOrNull()
?: DefaultData.coverRule
}
suspend fun searchCover(book: Book): String? {
@@ -266,6 +270,10 @@ object BookCover {
fun saveCoverRule(config: CoverRule) {
val json = GSON.toJson(config)
saveCoverRule(json)
}
fun saveCoverRule(json: String) {
CacheManager.put(coverRuleConfigKey, json)
}
@@ -4,6 +4,7 @@ import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import androidx.core.graphics.createBitmap
import io.legado.app.constant.AppLog
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@@ -121,18 +122,15 @@ class PdfFile(var book: Book) {
if (index >= renderer.pageCount) {
return null
}
return renderer.openPage(index)?.use { page ->
Bitmap.createBitmap(
return renderer.openPage(index).use { page ->
createBitmap(
SystemUtils.screenWidthPx,
(SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt(),
Bitmap.Config.ARGB_8888
)
.apply {
this.eraseColor(Color.WHITE)
page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
}
(SystemUtils.screenWidthPx.toDouble() * page.height / page.width).toInt()
).apply {
this.eraseColor(Color.WHITE)
page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
}
}
}
private fun getContent(chapter: BookChapter): String? =
@@ -168,7 +166,7 @@ class PdfFile(var book: Book) {
null
}
} catch (e: Exception) {
} catch (_: Exception) {
return null
}
}
@@ -308,6 +308,10 @@ abstract class BaseReadAloudService : BaseService(),
@SuppressLint("WakelockTimeout")
@CallSuper
open fun resumeReadAloud() {
resumeReadAloudInternal()
}
private fun resumeReadAloudInternal() {
pause = false
needResumeOnAudioFocusGain = false
needResumeOnCallStateIdle = false
@@ -674,12 +678,14 @@ abstract class BaseReadAloudService : BaseService(),
open fun prevChapter() {
toLast = false
resumeReadAloudInternal()
ReadBook.moveToPrevChapter(true, toLast = false)
}
open fun nextChapter() {
ReadBook.upReadTime()
AppLog.putDebug("${ReadBook.curTextChapter?.chapter?.title} 朗读结束跳转下一章并朗读")
resumeReadAloudInternal()
if (!ReadBook.moveToNextChapter(true)) {
stopSelf()
}
@@ -37,6 +37,7 @@ import io.legado.app.help.http.okHttpClient
import io.legado.app.model.ReadAloud
import io.legado.app.model.ReadBook
import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.ui.book.read.page.entities.TextChapter
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.printOnDebug
@@ -46,6 +47,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
@@ -197,8 +199,8 @@ class HttpReadAloudService : BaseReadAloudService(),
.take(10)
.toList()
contentList.forEach { content ->
coroutineContext.ensureActive()
val fileName = md5SpeakFileName(content)
currentCoroutineContext().ensureActive()
val fileName = md5SpeakFileName(content, textChapter)
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
if (speakText.isEmpty()) {
createSilentSound(fileName)
@@ -266,8 +268,8 @@ class HttpReadAloudService : BaseReadAloudService(),
.take(10)
.toList()
contentList.forEach { content ->
coroutineContext.ensureActive()
val fileName = md5SpeakFileName(content)
currentCoroutineContext().ensureActive()
val fileName = md5SpeakFileName(content, textChapter)
val speakText = content.replace(AppPattern.notReadAloudRegex, "")
val dataSourceFactory = createDataSourceFactory(httpTts, speakText)
val downloader = createDownloader(dataSourceFactory, fileName)
@@ -332,10 +334,10 @@ class HttpReadAloudService : BaseReadAloudService(),
speakSpeed = speechRate,
source = httpTts,
readTimeout = 300 * 1000L,
coroutineContext = coroutineContext
coroutineContext = currentCoroutineContext()
)
var response = analyzeUrl.getResponseAwait()
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
val checkJs = httpTts.loginCheckJs
if (checkJs?.isNotBlank() == true) {
response = analyzeUrl.evalJS(checkJs, response) as Response
@@ -353,7 +355,7 @@ class HttpReadAloudService : BaseReadAloudService(),
}
}
}
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
response.body.byteStream().let { stream ->
downloadErrorNo = 0
return stream
@@ -396,7 +398,7 @@ class HttpReadAloudService : BaseReadAloudService(),
return null
}
private fun md5SpeakFileName(content: String): String {
private fun md5SpeakFileName(content: String, textChapter: TextChapter? = this.textChapter): String {
return MD5Utils.md5Encode16(textChapter?.title ?: "") + "_" +
MD5Utils.md5Encode16("${ReadAloud.httpTTS?.url}-|-$speechRate-|-$content")
}
@@ -586,7 +588,7 @@ class HttpReadAloudService : BaseReadAloudService(),
return servicePendingIntent<HttpReadAloudService>(actionStr)
}
inner class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) {
class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) {
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
return C.TIME_UNSET
}
@@ -223,10 +223,11 @@ class ImportRssSourceDialog() : BaseDialogFragment(R.layout.dialog_recycler_view
binding.apply {
cbSourceName.isChecked = viewModel.selectStatus[holder.layoutPosition]
cbSourceName.text = item.sourceName
tvSourceState.text = if (viewModel.checkSources[holder.layoutPosition] != null) {
"已有"
} else {
""
val localSource = viewModel.checkSources[holder.layoutPosition]
tvSourceState.text = when {
localSource == null -> "新增"
item.lastUpdateTime > localSource.lastUpdateTime -> ""
else -> "已有"
}
}
}
@@ -188,7 +188,7 @@ class ImportRssSourceViewModel(app: Application) : BaseViewModel(app) {
allSources.forEach {
val has = appDb.rssSourceDao.getByKey(it.sourceUrl)
checkSources.add(has)
selectStatus.add(has == null)
selectStatus.add(has == null || has.lastUpdateTime < it.lastUpdateTime)
}
successLiveData.postValue(allSources.size)
}
@@ -17,6 +17,7 @@ import io.legado.app.databinding.DialogChangeCoverBinding
import io.legado.app.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
@@ -63,11 +64,10 @@ class ChangeCoverDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_change
private fun initData() {
lifecycleScope.launch {
repeatOnLifecycle(STARTED) {
viewModel.dataFlow.conflate().collect {
adapter.setItems(it)
delay(1000)
}
lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
viewModel.dataFlow.conflate().collect {
adapter.setItems(it)
delay(1000)
}
}
}
@@ -52,6 +52,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
/**
@@ -240,15 +241,11 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
}
lifecycleScope.launch {
repeatOnLifecycle(STARTED) {
viewModel.searchDataFlow.conflate().collect {
adapter.setItems(it)
lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
viewModel.searchDataFlow.conflate().collect {
adapter.setItems(it)
delay(1000)
binding.tvEmptyMsg.isVisible = it.isEmpty()
binding.recyclerView.isVisible = it.isNotEmpty()
delay(1000)
}
}
}
@@ -48,6 +48,7 @@ import io.legado.app.utils.visible
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -233,11 +234,11 @@ class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_c
binding.toolBar.menu.applyTint(requireContext())
}
lifecycleScope.launch {
repeatOnLifecycle(STARTED) {
viewModel.searchDataFlow.conflate().collect {
searchBookAdapter.setItems(it)
delay(1000)
}
lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
viewModel.searchDataFlow.conflate().collect {
searchBookAdapter.setItems(it)
delay(1000)
}
}
lifecycleScope.launch {
@@ -33,7 +33,7 @@ class AutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_auto_read)
val bottomDialog = (activity as ReadBookActivity).bottomDialog++
if (bottomDialog > 0) {
dismiss()
return
return@run
}
initOnChange()
initData()
@@ -26,20 +26,25 @@ import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector
import io.legado.app.ui.book.read.ReadBookActivity
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.utils.FileDoc
import io.legado.app.utils.FileUtils
import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.SelectImageContract
import io.legado.app.utils.compress.ZipUtils
import io.legado.app.utils.createFileIfNotExist
import io.legado.app.utils.createFileReplace
import io.legado.app.utils.createFolderReplace
import io.legado.app.utils.delete
import io.legado.app.utils.externalCache
import io.legado.app.utils.externalFiles
import io.legado.app.utils.find
import io.legado.app.utils.getFile
import io.legado.app.utils.inputStream
import io.legado.app.utils.isContentScheme
import io.legado.app.utils.launch
import io.legado.app.utils.longToast
import io.legado.app.utils.openInputStream
import io.legado.app.utils.openOutputStream
import io.legado.app.utils.outputStream
import io.legado.app.utils.parseToUri
@@ -238,68 +243,37 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
configDir.createFolderReplace()
val configFile = configDir.getFile("readConfig.json")
configFile.createFileReplace()
configFile.writeText(GSON.toJson(ReadBookConfig.getExportConfig()))
exportFiles.add(configFile)
val config = ReadBookConfig.getExportConfig()
val fontPath = ReadBookConfig.textFont
if (fontPath.isNotEmpty()) {
val fontName = FileUtils.getName(fontPath)
val fontInputStream =
fontPath.parseToUri().inputStream(requireContext()).getOrNull()
val fontDoc = FileDoc.fromFile(fontPath)
val fontName = fontDoc.name
val fontInputStream = fontDoc.openInputStream().getOrNull()
fontInputStream?.use {
val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
it.copyTo(fontExportFile.outputStream())
fontExportFile.outputStream().use { out ->
it.copyTo(out)
}
config.textFont = fontName
exportFiles.add(fontExportFile)
}
}
if (ReadBookConfig.durConfig.bgType == 2) {
val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStr)
val bgFile = File(ReadBookConfig.durConfig.bgStr)
if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName))
if (!bgExportFile.exists()) {
bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
}
}
if (ReadBookConfig.durConfig.bgTypeNight == 2) {
val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrNight)
val bgFile = File(ReadBookConfig.durConfig.bgStrNight)
if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName))
if (!bgExportFile.exists()) {
bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
}
}
if (ReadBookConfig.durConfig.bgTypeEInk == 2) {
val bgName = FileUtils.getName(ReadBookConfig.durConfig.bgStrEInk)
val bgFile = File(ReadBookConfig.durConfig.bgStrEInk)
if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName))
if (!bgExportFile.exists()) {
bgFile.copyTo(bgExportFile)
exportFiles.add(bgExportFile)
}
}
configFile.writeText(GSON.toJson(config))
exportFiles.add(configFile)
repeat(3) {
val path = ReadBookConfig.durConfig.getBgPath(it) ?: return@repeat
val bgExportFile = copyBgImage(path, configDir) ?: return@repeat
exportFiles.add(bgExportFile)
}
val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) {
if (uri.isContentScheme()) {
DocumentFile.fromTreeUri(requireContext(), uri)?.let { treeDoc ->
treeDoc.findFile(exportFileName)?.delete()
val out = treeDoc.createFile("", exportFileName)?.openOutputStream()
out?.use {
File(configZipPath).inputStream().use {
it.copyTo(out)
}
}
val exportDir = FileDoc.fromDir(uri)
exportDir.find(exportFileName)?.delete()
val exportFileDoc = exportDir.createFileIfNotExist(exportFileName)
exportFileDoc.openOutputStream().getOrThrow().use { out ->
File(configZipPath).inputStream().use {
it.copyTo(out)
}
} else {
val exportPath = FileUtils.getPath(File(uri.path!!), exportFileName)
FileUtils.delete(exportPath)
File(configZipPath).copyTo(FileUtils.createFileIfNotExist(exportPath))
}
}
}.onSuccess {
@@ -311,6 +285,19 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
}
}
private fun copyBgImage(path: String, configDir: File): File? {
val bgName = FileUtils.getName(path)
val bgFile = File(path)
if (bgFile.exists()) {
val bgExportFile = File(FileUtils.getPath(configDir, bgName))
if (!bgExportFile.exists()) {
bgFile.copyTo(bgExportFile)
return bgExportFile
}
}
return null
}
@SuppressLint("InflateParams")
private fun importNetConfigAlert() {
alert("输入地址") {
@@ -8,6 +8,7 @@ import android.util.Size
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.AppPattern.noWordCountRegex
import io.legado.app.constant.PageAnim
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
@@ -83,6 +84,7 @@ class TextChapterLayout(
private val useZhLayout = ReadBookConfig.useZhLayout
private val isMiddleTitle = ReadBookConfig.isMiddleTitle
private val textFullJustify = ReadBookConfig.textFullJustify
private val pageAnim = book.getPageAnim()
private val titleSegType = ReadBookConfig.titleSegType
private val titleSegDistance = ReadBookConfig.titleSegDistance
private val titleSegFlag = ReadBookConfig.titleSegFlag
@@ -476,6 +478,13 @@ class TextChapterLayout(
Book.imgStyleFull -> {
width = visibleWidth
height = size.height * visibleWidth / size.width
if (pageAnim != PageAnim.scrollPageAnim && height > visibleHeight - durY) {
if (height > visibleHeight) {
width = width * visibleHeight / height
height = visibleHeight
}
prepareNextPageIfNeed(durY + height)
}
}
Book.imgStyleSingle -> {
@@ -103,7 +103,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
override var sortAscending = true
private set
private var snackBar: Snackbar? = null
private var showDuplicationSource = false
private var groupSourcesByDomain = false
private val hostMap = hashMapOf<String, String>()
private val qrResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
@@ -256,9 +256,9 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
searchView.setQuery(getString(R.string.disabled_explore), true)
}
R.id.menu_show_same_source -> {
R.id.menu_group_sources_by_domain -> {
item.isChecked = !item.isChecked
showDuplicationSource = item.isChecked
groupSourcesByDomain = item.isChecked
adapter.showSourceHost = item.isChecked
upBookSource(searchView.query?.toString())
}
@@ -334,7 +334,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
}
}.map { data ->
hostMap.clear()
if (showDuplicationSource) {
if (groupSourcesByDomain) {
data.sortedWith(
compareBy<BookSourcePart> { getSourceHost(it.bookSourceUrl) == "#" }
.thenBy { getSourceHost(it.bookSourceUrl) }
@@ -388,7 +388,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
}.flowOn(IO).conflate().collect { data ->
adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking)
itemTouchCallback.isCanDrag =
sort == BookSourceSort.Default && !showDuplicationSource
sort == BookSourceSort.Default && !groupSourcesByDomain
delay(500)
}
}
@@ -55,6 +55,7 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx
import kotlin.coroutines.coroutineContext
import androidx.core.content.edit
import kotlinx.coroutines.currentCoroutineContext
class BackupConfigFragment : PreferenceFragment(),
SharedPreferences.OnSharedPreferenceChangeListener,
@@ -363,7 +364,7 @@ class BackupConfigFragment : PreferenceFragment(),
context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
}
if (names.isNotEmpty()) {
coroutineContext.ensureActive()
currentCoroutineContext().ensureActive()
withContext(Main) {
context.selector(
title = context.getString(R.string.select_restore_file),
@@ -285,6 +285,9 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
* 备份同步
*/
private fun backupSync() {
if (!AppConfig.autoCheckNewBackup) {
return
}
lifecycleScope.launch {
val lastBackupFile =
withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch
@@ -10,6 +10,8 @@ import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppCacheManager
import io.legado.app.help.RuleComplete
import io.legado.app.help.http.CookieStore
import io.legado.app.help.source.removeSortCache
import io.legado.app.model.SharedJsScope
import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getClipText
@@ -41,6 +43,16 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
if (source.sourceName.isBlank() || source.sourceName.isBlank()) {
throw NoStackTraceException(context.getString(R.string.non_null_name_url))
}
val oldSource = rssSource ?: RssSource()
if (!source.equal(oldSource)) {
source.lastUpdateTime = System.currentTimeMillis()
if (oldSource.sortUrl != source.sortUrl) {
oldSource.removeSortCache()
}
if (oldSource.jsLib != source.jsLib) {
SharedJsScope.remove(oldSource.jsLib)
}
}
rssSource?.let {
appDb.rssSourceDao.delete(it)
//更新收藏的源地址
@@ -457,7 +457,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
* title 时间计算工具类
*/
private object Utils {
@Suppress("ConstPropertyName")
private const val mSeparator = ' '
/**
@@ -762,8 +762,8 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int)
var fileSize: Long = 0
if (mostLongUsedFile != null) {
fileSize = calculateSize(mostLongUsedFile!!)
if (mostLongUsedFile!!.delete()) {
fileSize = calculateSize(mostLongUsedFile)
if (mostLongUsedFile.delete()) {
lastUsageDates.remove(mostLongUsedFile)
}
}
@@ -81,6 +81,10 @@ data class FileDoc(
return fromUri(path.toUri(), false)
}
fun fromDir(uri: Uri): FileDoc {
return fromUri(uri, true)
}
fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
if (uri.isContentScheme()) {
val doc = if (isDir) {
@@ -92,7 +96,7 @@ data class FileDoc(
if (it.moveToFirst()) {
val lUriColum = it.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
val lUri = it.getString(lUriColum)
DocumentFile.fromSingleUri(appCtx, Uri.parse(lUri))!!
DocumentFile.fromSingleUri(appCtx, lUri.toUri())!!
} else {
DocumentFile.fromSingleUri(appCtx, uri)!!
}
@@ -241,7 +241,7 @@ object ZipUtils {
if (!entryFile.canonicalPath.startsWith(dir.canonicalPath)) {
throw SecurityException("压缩文件只能解压到指定路径")
}
if (entry!!.isDirectory) {
if (entry.isDirectory) {
if (!entryFile.exists()) {
entryFile.mkdirs()
}
@@ -286,7 +286,7 @@ object ZipUtils {
if (entry!!.isDirectory) {
continue
}
val fileName = entry!!.name
val fileName = entry.name
if (filter != null && filter.invoke(fileName))
fileNames.add(fileName)
}