[优化] 同步上游更新 (@821938089)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user