[优化] 同步上游更新 (@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
@@ -18,6 +18,7 @@
> `{"example":"https://www.example.com/js/example.js", ...}` 自动复用已经下载的js文件
> 注意此处定义的函数可能会被多个线程同时调用,在函数里的全局变量内容将会共享使用,对其进行修改可能会出现竞争问题
> 函数内不可声明全局变量,函数外的全局变量不可再赋值,否则会抛出 `无法修改密封对象的属性` 异常
* 并发率
> 并发限制,单位ms,可填写两种格式
+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)
}
+2 -2
View File
@@ -129,9 +129,9 @@
app:showAsAction="never" />
<item
android:id="@+id/menu_show_same_source"
android:id="@+id/menu_group_sources_by_domain"
android:checkable="true"
android:title="@string/show_same_source"
android:title="@string/group_sources_by_domain"
app:showAsAction="never" />
<item
+3 -1
View File
@@ -1180,7 +1180,7 @@
<string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
<string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
<string name="show_same_source">显示重复书源</string>
<string name="group_sources_by_domain">按域名分组显示</string>
<string name="theme_config">主题配置</string>
<string name="show_manga_ui">漫画浏览</string>
<string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1212,4 +1212,6 @@
<string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
<string name="auto_check_new_backup_t">自动检查新备份</string>
<string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
</resources>
+3 -1
View File
@@ -1182,7 +1182,7 @@
<string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
<string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
<string name="show_same_source">显示重复书源</string>
<string name="group_sources_by_domain">按域名分组显示</string>
<string name="theme_config">主题配置</string>
<string name="show_manga_ui">漫画浏览</string>
<string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1214,4 +1214,6 @@
<string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
<string name="auto_check_new_backup_t">自动检查新备份</string>
<string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
</resources>
+3 -1
View File
@@ -1185,7 +1185,7 @@
<string name="read_aloud_read_phone_state_permission_rationale">阅读需要读取手机状态实现来电期间暂停朗读功能</string>
<string name="read_aloud_by_media_button_title">耳机按键启动朗读</string>
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
<string name="show_same_source">显示重复书源</string>
<string name="group_sources_by_domain">按域名分组显示</string>
<string name="theme_config">主题配置</string>
<string name="show_manga_ui">漫画浏览</string>
<string name="disable_manga_scale">禁用漫画缩放</string>
@@ -1344,6 +1344,7 @@
<string name="underline_height">线段高度</string>
<string name="title_bar_mode">菜单标题显示方式</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
<string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
<string name="replace_purify_badge">计数替换净化</string>
<string name="page_anim_fade">渐变</string>
<string name="show_bookshelf_tab_menu">显示更多分组按钮</string>
@@ -1362,4 +1363,5 @@
<string name="enable_slider_vibrator">滑动进度条时震动</string>
<string name="edit_remark">编辑备注</string>
<string name="book_remark">书籍备注</string>
<string name="auto_check_new_backup_t">自动检查新备份</string>
</resources>
+3 -1
View File
@@ -1188,7 +1188,7 @@
<string name="read_aloud_read_phone_state_permission_rationale">Reading requires phone state access to pause reading aloud during calls</string>
<string name="read_aloud_by_media_button_title">Start Read Aloud via Headset Button</string>
<string name="read_aloud_by_media_button_summary">Use headset button to start reading aloud</string>
<string name="show_same_source">Show Duplicate Sources</string>
<string name="group_sources_by_domain">按域名分组显示</string>
<string name="theme_config">Theme Configuration</string>
<string name="show_manga_ui">Manga Viewer</string>
<string name="disable_manga_scale">Disable Manga Zoom</string>
@@ -1347,6 +1347,8 @@
<string name="underline_height">线段高度</string>
<string name="title_bar_mode">菜单标题显示方式</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
<string name="auto_check_new_backup_t">自动检查新备份</string>
<string name="auto_check_new_backup_s">打开软件时检查是否有新备份,有新备份时提示是否更新</string>
<string name="replace_purify_badge">计数替换净化</string>
<string name="page_anim_fade">渐变</string>
<string name="show_bookshelf_tab_menu">显示更多分组按钮</string>
@@ -60,6 +60,15 @@
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.SwitchPreference
android:defaultValue="true"
android:key="autoCheckNewBackup"
android:summary="@string/auto_check_new_backup_s"
android:title="@string/auto_check_new_backup_t"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
</io.legado.app.lib.prefs.PreferenceCategory>
<io.legado.app.lib.prefs.PreferenceCategory
+11 -11
View File
@@ -4,18 +4,18 @@ accompanistWebview = "0.36.0"
animation = "1.10.0"
biometric = "1.2.0"
coilCompose = "2.7.0"
composeBom = "2025.12.00"
composeBom = "2025.12.01"
constraintlayoutCompose = "1.1.1"
coreSplashscreen = "1.2.0"
datastorePreferences = "1.2.0"
foundation = "1.10.0"
kotlin = "2.2.21"
kotlin = "2.3.0"
kotlinxCoroutinesAndroid = "1.10.2"
ksp = "2.2.21-2.0.4"
agp = "8.13.1"
agp = "8.13.2"
appcompat = "1.7.1"
colorpicker = "1.1.0"
commonsText = "1.14.0"
commonsText = "1.15.0"
constraintlayout = "2.2.1"
core = "1.17.0"
firebaseBom = "34.6.0"
@@ -39,25 +39,25 @@ lifecycleViewmodelCompose = "2.10.0"
liveeventbus = "1.8.14"
markdownCompose = "0.5.0"
markwon = "4.6.2"
material = "1.14.0-alpha07"
material3 = "1.5.0-alpha10"
material = "1.14.0-alpha08"
material3 = "1.5.0-alpha11"
material3IconsExtended = "1.2.0"
materialIconsExtended = "1.7.8"
media = "1.7.1"
media3 = "1.8.0"
nanoHttpd = "2.3.1"
navigationCompose = "2.9.6"
okhttp = "5.2.0"
okhttp = "5.3.2"
palette = "1.0.0"
preference = "1.2.1"
protobufJavalite = "4.26.1"
quickChineseTransfer = "0.2.16"
room = "2.8.3"
room = "2.8.4"
splitties = "3.0.0"
rhino = "1.8.0"
rhino = "1.8.1"
desugar = "2.1.5"
activity = "1.12.1"
activity = "1.12.2"
#kotlinxSerialization = "1.8.0"
startupRuntime = "1.2.0"
swiperefreshlayout = "1.2.0"
@@ -66,7 +66,7 @@ recyclerview = "1.4.0"
#noinspection GradleDependency
uiViewbinding = "1.9.4"
viewpager2 = "1.1.0"
webkit = "1.14.0"
webkit = "1.15.0"
collection = "1.5.0"
zxingLite = "3.3.0"
@@ -206,7 +206,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
thiz1 = Context.toObject(thiz1, topLevel)
}
val engineScope = getRuntimeScope(context)
val localScope = if (thiz1 != null) thiz1 as Scriptable else engineScope
val localScope = thiz1 ?: engineScope
val obj = ScriptableObject.getProperty(localScope, name) as? Function
?: throw NoSuchMethodException("no such method: $name")
var scope = obj.parentScope
@@ -229,7 +229,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
override fun <T> getInterface(clazz: Class<T>): T? {
return try {
implementor.getInterface(null, clazz)
} catch (var3: ScriptException) {
} catch (_: ScriptException) {
null
}
}
@@ -240,7 +240,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
} else {
try {
implementor.getInterface(obj, paramClass)
} catch (var4: ScriptException) {
} catch (_: ScriptException) {
null
}
}
@@ -321,7 +321,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
override fun makeContext(): Context {
val cx = RhinoContext(this)
cx.languageVersion = Context.VERSION_ES6
cx.setInterpretedMode(true)
cx.isInterpretedMode = true
cx.setClassShutter(RhinoClassShutter)
cx.wrapFactory = RhinoWrapFactory
cx.instructionObserverThreshold = 10000
@@ -393,7 +393,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
if (System.getSecurityManager() != null) {
try {
AccessController.checkPermission(AllPermission())
} catch (var6: AccessControlException) {
} catch (_: AccessControlException) {
accessContext = AccessController.getContext()
}
}
@@ -413,7 +413,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
obj1 = Context.toObject(obj1, topLevel)
}
val engineScope = getRuntimeScope(context)
val localScope = if (obj1 != null) obj1 as Scriptable else engineScope
val localScope = obj1 ?: engineScope
val methods = clazz.methods
val methodsSize = methods.size
for (index in 0 until methodsSize) {
+2
View File
@@ -68,6 +68,7 @@
import API from '@api'
import { CircleCheckFilled, Edit } from '@element-plus/icons-vue'
import hotkeys from 'hotkeys-js'
import { getSourceName, isInvaildSource, normalizeSource } from '../utils/souce'
import { getSourceName, isInvaildSource } from '../utils/souce'
const store = useSourceStore()
@@ -168,6 +169,7 @@ const redo = () => {
const saveSource = () => {
const source = store.currentSource
if (isInvaildSource(source)) {
normalizeSource(source)
API.saveSource(source).then(({ data }) => {
const sourceName = getSourceName(source)
if (data.isSuccess) {
+4 -4
View File
@@ -13,8 +13,8 @@ const emptySource = isBookSource ? emptyBookSource : emptyRssSource
export const useSourceStore = defineStore('source', {
state: () => {
return {
bookSources: [] as BookSoure[], // 临时存放所有书源,
rssSources: [] as RssSource[], // 临时存放所有订阅源
bookSources: shallowRef([] as BookSoure[]), // 临时存放所有书源,
rssSources: shallowRef([] as RssSource[]), // 临时存放所有订阅源
savedSources: [] as Source[], // 批量保存到阅读app成功的源
currentSource: JSON.parse(JSON.stringify(emptySource)) as Source, // 当前编辑的源
currentTab: localStorage.getItem('tabName') || 'editTab',
@@ -51,9 +51,9 @@ export const useSourceStore = defineStore('source', {
//拉取源后保存
saveSources(data: Source[]) {
if (isBookSource) {
this.bookSources = data as BookSoure[]
this.bookSources = markRaw(data) as BookSoure[]
} else {
this.rssSources = data as RssSource[]
this.rssSources = markRaw(data) as RssSource[]
}
},
//批量推送
+16 -1
View File
@@ -49,12 +49,27 @@ export const convertSourcesToMap = (sources: Source[]): Map<string, Source> => {
return map
}
export const normalizeSource = (source: any) => {
for (const key in source) {
const value = source[key]
if (
value === '' ||
value === null ||
(typeof value === 'string' && !value.trim())
) {
delete source[key]
} else if (value instanceof Object) {
normalizeSource(value)
}
}
}
export const emptyBookSource = {
ruleSearch: {},
ruleBookInfo: {},
ruleToc: {},
ruleContent: {},
ruleReview: {},
//ruleReview: {},
ruleExplore: {},
} as BookSoure
export const emptyRssSource = {} as RssSource