From 060ec9ce1ae6f96da9072e4a375cbba8dc7825c4 Mon Sep 17 00:00:00 2001 From: HapeLee <1321903405@qq.com> Date: Sat, 20 Dec 2025 03:33:17 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=B8=8A=E6=B8=B8=E6=9B=B4=E6=96=B0=20(@821938089)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/assets/web/help/md/ruleHelp.md | 1 + app/src/main/java/io/legado/app/App.kt | 4 + .../java/io/legado/app/constant/PreferKey.kt | 2 +- .../io/legado/app/data/entities/rule/RowUi.kt | 2 +- .../java/io/legado/app/help/JsExtensions.kt | 2 +- .../io/legado/app/help/book/ContentHelp.kt | 50 ++++++----- .../io/legado/app/help/config/AppConfig.kt | 2 + .../legado/app/help/config/ReadBookConfig.kt | 57 +++++++++++- .../io/legado/app/help/config/ThemeConfig.kt | 19 +++- .../io/legado/app/help/http/OkHttpUtils.kt | 7 +- .../java/io/legado/app/help/storage/Backup.kt | 53 ++++++----- .../io/legado/app/help/storage/Restore.kt | 9 ++ .../java/io/legado/app/lib/webdav/WebDav.kt | 15 ++-- .../java/io/legado/app/model/AudioPlay.kt | 20 +++-- .../java/io/legado/app/model/BookCover.kt | 10 ++- .../io/legado/app/model/localBook/PdfFile.kt | 20 ++--- .../app/service/BaseReadAloudService.kt | 6 ++ .../app/service/HttpReadAloudService.kt | 20 +++-- .../ui/association/ImportRssSourceDialog.kt | 9 +- .../association/ImportRssSourceViewModel.kt | 2 +- .../ui/book/changecover/ChangeCoverDialog.kt | 10 +-- .../changesource/ChangeBookSourceDialog.kt | 13 ++- .../changesource/ChangeChapterSourceDialog.kt | 11 +-- .../app/ui/book/read/config/AutoReadDialog.kt | 2 +- .../ui/book/read/config/BgTextConfigDialog.kt | 89 ++++++++----------- .../read/page/provider/TextChapterLayout.kt | 9 ++ .../book/source/manage/BookSourceActivity.kt | 10 +-- .../app/ui/config/BackupConfigFragment.kt | 3 +- .../io/legado/app/ui/main/MainActivity.kt | 3 + .../rss/source/edit/RssSourceEditViewModel.kt | 12 +++ .../main/java/io/legado/app/utils/ACache.kt | 6 +- .../io/legado/app/utils/FileDocExtensions.kt | 6 +- .../io/legado/app/utils/compress/ZipUtils.kt | 4 +- app/src/main/res/menu/book_source.xml | 4 +- app/src/main/res/values-zh-rHK/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values-zh/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/pref_config_backup.xml | 9 ++ gradle/libs.versions.toml | 22 ++--- .../com/script/rhino/RhinoScriptEngine.kt | 12 +-- modules/web/src/components/ToolBar.vue | 2 + modules/web/src/store/sourceStore.ts | 8 +- modules/web/src/utils/souce.ts | 17 +++- 44 files changed, 373 insertions(+), 205 deletions(-) diff --git a/app/src/main/assets/web/help/md/ruleHelp.md b/app/src/main/assets/web/help/md/ruleHelp.md index 3841b3bd7..54c1845bb 100644 --- a/app/src/main/assets/web/help/md/ruleHelp.md +++ b/app/src/main/assets/web/help/md/ruleHelp.md @@ -18,6 +18,7 @@ > `{"example":"https://www.example.com/js/example.js", ...}` 自动复用已经下载的js文件 > 注意此处定义的函数可能会被多个线程同时调用,在函数里的全局变量内容将会共享使用,对其进行修改可能会出现竞争问题 +> 函数内不可声明全局变量,函数外的全局变量不可再赋值,否则会抛出 `无法修改密封对象的属性` 异常 * 并发率 > 并发限制,单位ms,可填写两种格式 diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index 5b17e1827..cd476715a 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -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 -> { diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 4fd90cd61..f2b6d4612 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -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" diff --git a/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt b/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt index 482d2aa3f..8946707eb 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/RowUi.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index b5b16549a..63a53414f 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -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}" diff --git a/app/src/main/java/io/legado/app/help/book/ContentHelp.kt b/app/src/main/java/io/legado/app/help/book/ContentHelp.kt index ed49cb9a2..1f2cd8fd4 100644 --- a/app/src/main/java/io/legado/app/help/book/ContentHelp.kt +++ b/app/src/main/java/io/legado/app/help/book/ContentHelp.kt @@ -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 { val result = ArrayList() - 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 */ - private fun seekIndexs( + private fun seekIndexes( str: String, key: String, from: Int, @@ -514,14 +519,18 @@ object ContentHelp { val list = ArrayList() 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() // 限制字典的长度 diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index d9f8e5664..f71bc0418 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -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") diff --git a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt index 246cb9bb5..8a4429dad 100644 --- a/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt @@ -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() + 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(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() diff --git a/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt index 4fda6e1cb..987ff6981 100644 --- a/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/ThemeConfig.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt index 1134967e8..d16092d0a 100644 --- a/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt +++ b/app/src/main/java/io/legado/app/help/http/OkHttpUtils.kt @@ -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, encoded: Boolean = false) { val formBody = FormBody.Builder() form.forEach { diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index cc1d92a28..14c6a05fa 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -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, fileName: String, path: String) { - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() withContext(IO) { if (list.isNotEmpty()) { LogUtils.d(TAG, "阅读备份 $fileName 列表大小 ${list.size}") diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index de3ab4676..1b8421e0b 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -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 { diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index d7b4fad10..3f96f0546 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -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 } diff --git a/app/src/main/java/io/legado/app/model/AudioPlay.kt b/app/src/main/java/io/legado/app/model/AudioPlay.kt index ef37148b4..6749b8660 100644 --- a/app/src/main/java/io/legado/app/model/AudioPlay.kt +++ b/app/src/main/java/io/legado/app/model/AudioPlay.kt @@ -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..(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) } diff --git a/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt b/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt index 9723e3a1c..1a91067af 100644 --- a/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/PdfFile.kt @@ -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 } } diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index bf74a8a2b..8b822ceac 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -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() } diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index fcc872b85..bfc684c4b 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -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(actionStr) } - inner class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) { + class CustomLoadErrorHandlingPolicy : DefaultLoadErrorHandlingPolicy(0) { override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long { return C.TIME_UNSET } diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt index 8c1baf617..f82259cd8 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt @@ -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 -> "已有" } } } diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt index 0d10f7779..6f3d35cd0 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceViewModel.kt @@ -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) } diff --git a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt index 3cc83fd09..cfc9591f8 100644 --- a/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changecover/ChangeCoverDialog.kt @@ -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) } } } diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt index 8c8a15b2c..ab8f1f178 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeBookSourceDialog.kt @@ -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) - } } } diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt index a4b58662f..2b68e12aa 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeChapterSourceDialog.kt @@ -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 { diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt index 504945673..f1c5c8bfc 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/AutoReadDialog.kt @@ -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() diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index 07b1f1b7d..68b8b5cac 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -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("输入地址") { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt index 5a4d5936e..018a59c7f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt @@ -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 -> { diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index 9a18c9452..b90d28af7 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -103,7 +103,7 @@ class BookSourceActivity : VMBaseActivity() private val qrResult = registerForActivityResult(QrCodeResult()) { it ?: return@registerForActivityResult @@ -256,9 +256,9 @@ class BookSourceActivity : VMBaseActivity { + 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 hostMap.clear() - if (showDuplicationSource) { + if (groupSourcesByDomain) { data.sortedWith( compareBy { getSourceHost(it.bookSourceUrl) == "#" } .thenBy { getSourceHost(it.bookSourceUrl) } @@ -388,7 +388,7 @@ class BookSourceActivity : VMBaseActivity adapter.setItems(data, adapter.diffItemCallback, !Debug.isChecking) itemTouchCallback.isCanDrag = - sort == BookSourceSort.Default && !showDuplicationSource + sort == BookSourceSort.Default && !groupSourcesByDomain delay(500) } } diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt index a69495960..c3076e27a 100644 --- a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -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), diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 0556b0588..d8ebe4be7 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -285,6 +285,9 @@ open class MainActivity : VMBaseActivity(), * 备份同步 */ private fun backupSync() { + if (!AppConfig.autoCheckNewBackup) { + return + } lifecycleScope.launch { val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt index 5db8e4b34..1883d2213 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditViewModel.kt @@ -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) //更新收藏的源地址 diff --git a/app/src/main/java/io/legado/app/utils/ACache.kt b/app/src/main/java/io/legado/app/utils/ACache.kt index 214d11ad2..b2a03184c 100644 --- a/app/src/main/java/io/legado/app/utils/ACache.kt +++ b/app/src/main/java/io/legado/app/utils/ACache.kt @@ -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) } } diff --git a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt index 2f8026e94..56abc43b8 100644 --- a/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/FileDocExtensions.kt @@ -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)!! } diff --git a/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt index 152421c9e..26461a846 100644 --- a/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt +++ b/app/src/main/java/io/legado/app/utils/compress/ZipUtils.kt @@ -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) } diff --git a/app/src/main/res/menu/book_source.xml b/app/src/main/res/menu/book_source.xml index 8b2397efc..d24e5b4f9 100644 --- a/app/src/main/res/menu/book_source.xml +++ b/app/src/main/res/menu/book_source.xml @@ -129,9 +129,9 @@ app:showAsAction="never" /> 阅读需要读取手机状态实现来电期间暂停朗读功能 耳机按键启动朗读 通过耳机按键来启动朗读 - 显示重复书源 + 按域名分组显示 主题配置 漫画浏览 禁用漫画缩放 @@ -1212,4 +1212,6 @@ 刷新发现 填充刘海区域 是否确认开始缓存? + 自动检查新备份 + 打开软件时检查是否有新备份,有新备份时提示是否更新 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7764a0571..04d2146cd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1182,7 +1182,7 @@ 阅读需要读取手机状态实现来电期间暂停朗读功能 耳机按键启动朗读 通过耳机按键来启动朗读 - 显示重复书源 + 按域名分组显示 主题配置 漫画浏览 禁用漫画缩放 @@ -1214,4 +1214,6 @@ 刷新发现 填充刘海区域 是否确认开始缓存? + 自动检查新备份 + 打开软件时检查是否有新备份,有新备份时提示是否更新 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 60b47b6f1..78453d4d9 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1185,7 +1185,7 @@ 阅读需要读取手机状态实现来电期间暂停朗读功能 耳机按键启动朗读 通过耳机按键来启动朗读 - 显示重复书源 + 按域名分组显示 主题配置 漫画浏览 禁用漫画缩放 @@ -1344,6 +1344,7 @@ 线段高度 菜单标题显示方式 是否确认开始缓存? + 打开软件时检查是否有新备份,有新备份时提示是否更新 计数替换净化 渐变 显示更多分组按钮 @@ -1362,4 +1363,5 @@ 滑动进度条时震动 编辑备注 书籍备注 + 自动检查新备份 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9844ce5b7..912eeca50 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1188,7 +1188,7 @@ Reading requires phone state access to pause reading aloud during calls Start Read Aloud via Headset Button Use headset button to start reading aloud - Show Duplicate Sources + 按域名分组显示 Theme Configuration Manga Viewer Disable Manga Zoom @@ -1347,6 +1347,8 @@ 线段高度 菜单标题显示方式 是否确认开始缓存? + 自动检查新备份 + 打开软件时检查是否有新备份,有新备份时提示是否更新 计数替换净化 渐变 显示更多分组按钮 diff --git a/app/src/main/res/xml/pref_config_backup.xml b/app/src/main/res/xml/pref_config_backup.xml index 3a52532c6..bef5f2bc6 100644 --- a/app/src/main/res/xml/pref_config_backup.xml +++ b/app/src/main/res/xml/pref_config_backup.xml @@ -60,6 +60,15 @@ app:allowDividerBelow="false" app:iconSpaceReserved="false" /> + + getInterface(clazz: Class): 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) { diff --git a/modules/web/src/components/ToolBar.vue b/modules/web/src/components/ToolBar.vue index a627594c1..5f3d9e54e 100644 --- a/modules/web/src/components/ToolBar.vue +++ b/modules/web/src/components/ToolBar.vue @@ -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) { diff --git a/modules/web/src/store/sourceStore.ts b/modules/web/src/store/sourceStore.ts index 0fd954eab..a79007db1 100644 --- a/modules/web/src/store/sourceStore.ts +++ b/modules/web/src/store/sourceStore.ts @@ -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[] } }, //批量推送 diff --git a/modules/web/src/utils/souce.ts b/modules/web/src/utils/souce.ts index 355b2dd50..f0114d29a 100644 --- a/modules/web/src/utils/souce.ts +++ b/modules/web/src/utils/souce.ts @@ -49,12 +49,27 @@ export const convertSourcesToMap = (sources: Source[]): Map => { 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