Merge pull request #2457 from 821938089/little-fix

一些优化
pull/2459/head
Horis 3 years ago committed by GitHub
commit 7311ab38af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      app/build.gradle
  2. 7
      app/src/main/java/io/legado/app/base/BaseService.kt
  3. 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  4. 2
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  5. 7
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  6. 6
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  7. 1
      app/src/main/java/io/legado/app/help/coroutine/Coroutine.kt
  8. 16
      app/src/main/java/io/legado/app/model/ReadAloud.kt
  9. 2
      app/src/main/java/io/legado/app/model/ReadBook.kt
  10. 60
      app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt
  11. 11
      app/src/main/java/io/legado/app/service/BaseReadAloudService.kt
  12. 145
      app/src/main/java/io/legado/app/service/HttpReadAloudService.kt
  13. 3
      app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt
  14. 3
      app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt
  15. 1
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  16. 10
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt
  17. 7
      app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt
  18. 25
      app/src/main/java/io/legado/app/utils/EventBusExtensions.kt
  19. 2
      app/src/main/res/values-es-rES/strings.xml
  20. 2
      app/src/main/res/values-ja-rJP/strings.xml
  21. 2
      app/src/main/res/values-pt-rBR/strings.xml
  22. 2
      app/src/main/res/values-zh-rHK/strings.xml
  23. 2
      app/src/main/res/values-zh-rTW/strings.xml
  24. 2
      app/src/main/res/values-zh/strings.xml
  25. 2
      app/src/main/res/values/strings.xml
  26. 7
      app/src/main/res/xml/pref_config_backup.xml
  27. 7
      app/src/main/res/xml/pref_config_read.xml

@ -170,6 +170,7 @@ dependencies {
//lifecycle //lifecycle
def lifecycle_version = '2.5.1' def lifecycle_version = '2.5.1'
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version") implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version")
implementation("androidx.lifecycle:lifecycle-service:$lifecycle_version")
//compose //compose
// Integration with activities // Integration with activities

@ -1,15 +1,15 @@
package io.legado.app.base package io.legado.app.base
import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.lifecycle.LifecycleService
import io.legado.app.help.LifecycleHelp import io.legado.app.help.LifecycleHelp
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
abstract class BaseService : Service(), CoroutineScope by MainScope() { abstract class BaseService : LifecycleService(), CoroutineScope by MainScope() {
fun <T> execute( fun <T> execute(
scope: CoroutineScope = this, scope: CoroutineScope = this,
@ -30,7 +30,8 @@ abstract class BaseService : Service(), CoroutineScope by MainScope() {
stopSelf() stopSelf()
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null return null
} }

@ -30,4 +30,5 @@ object EventBus {
const val FILE_SOURCE_DOWNLOAD_DONE = "fileSourceDownloadDone" const val FILE_SOURCE_DOWNLOAD_DONE = "fileSourceDownloadDone"
const val UPDATE_READ_ACTION_BAR = "updateReadActionBar" const val UPDATE_READ_ACTION_BAR = "updateReadActionBar"
const val UP_SEEK_BAR = "upSeekBar" const val UP_SEEK_BAR = "upSeekBar"
const val READ_ALOUD_PLAY = "readAloudPlay"
} }

@ -117,6 +117,8 @@ object PreferKey {
const val progressBarBehavior = "progressBarBehavior" const val progressBarBehavior = "progressBarBehavior"
const val sourceEditMaxLine = "sourceEditMaxLine" const val sourceEditMaxLine = "sourceEditMaxLine"
const val ttsTimer = "ttsTimer" const val ttsTimer = "ttsTimer"
const val noAnimScrollPage = "noAnimScrollPage"
const val webDavDeviceName = "webDavDeviceName"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -68,7 +68,12 @@ object AppWebDav {
get() { get() {
val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) val backupDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
.format(Date(System.currentTimeMillis())) .format(Date(System.currentTimeMillis()))
return "backup${backupDate}.zip" val deviceName = AppConfig.webDavDeviceName
return if (deviceName?.isNotBlank() == true) {
"backup${backupDate}-${deviceName}.zip"
} else {
"backup${backupDate}.zip"
}
} }
suspend fun upConfig() { suspend fun upConfig() {

@ -2,6 +2,7 @@ package io.legado.app.help.config
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@ -184,6 +185,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value) appCtx.putPrefBoolean(PreferKey.ttsFollowSys, value)
} }
val noAnimScrollPage: Boolean
get() = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
const val defaultSpeechRate = 5 const val defaultSpeechRate = 5
var ttsSpeechRate: Int var ttsSpeechRate: Int
@ -334,6 +338,8 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val webDavDir get() = appCtx.getPrefString(PreferKey.webDavDir, "legado") val webDavDir get() = appCtx.getPrefString(PreferKey.webDavDir, "legado")
val webDavDeviceName get() = appCtx.getPrefString(PreferKey.webDavDeviceName, Build.MODEL)
val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog) val recordLog get() = appCtx.getPrefBoolean(PreferKey.recordLog)
val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false) val loadCoverOnlyWifi get() = appCtx.getPrefBoolean(PreferKey.loadCoverOnlyWifi, false)

@ -99,6 +99,7 @@ class Coroutine<T>(
return this@Coroutine return this@Coroutine
} }
// 如果协程被取消,有可能会不执行
fun onFinally( fun onFinally(
context: CoroutineContext? = null, context: CoroutineContext? = null,
block: suspend CoroutineScope.() -> Unit block: suspend CoroutineScope.() -> Unit

@ -2,6 +2,8 @@ package io.legado.app.model
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import io.legado.app.constant.EventBus
import io.legado.app.constant.IntentAction import io.legado.app.constant.IntentAction
import io.legado.app.data.appDb import io.legado.app.data.appDb
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
@ -10,6 +12,7 @@ import io.legado.app.service.BaseReadAloudService
import io.legado.app.service.HttpReadAloudService import io.legado.app.service.HttpReadAloudService
import io.legado.app.service.TTSReadAloudService import io.legado.app.service.TTSReadAloudService
import io.legado.app.utils.StringUtils import io.legado.app.utils.StringUtils
import io.legado.app.utils.postEvent
import splitties.init.appCtx import splitties.init.appCtx
object ReadAloud { object ReadAloud {
@ -50,6 +53,19 @@ object ReadAloud {
context.startService(intent) context.startService(intent)
} }
fun playByEventBus(
play: Boolean = true,
pageIndex: Int = ReadBook.durPageIndex,
startPos: Int = 0
) {
val bundle = Bundle().apply {
putBoolean("play", play)
putInt("pageIndex", pageIndex)
putInt("startPos", startPos)
}
postEvent(EventBus.READ_ALOUD_PLAY, bundle)
}
fun pause(context: Context) { fun pause(context: Context) {
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
val intent = Intent(context, aloudClass) val intent = Intent(context, aloudClass)

@ -234,7 +234,7 @@ object ReadBook : CoroutineScope by MainScope() {
private fun curPageChanged() { private fun curPageChanged() {
callBack?.pageChanged() callBack?.pageChanged()
if (BaseReadAloudService.isRun) { if (BaseReadAloudService.isRun) {
readAloud(!BaseReadAloudService.pause) ReadAloud.playByEventBus(!BaseReadAloudService.pause)
} }
upReadTime() upReadTime()
preDownload() preDownload()

@ -354,6 +354,7 @@ class AnalyzeUrl(
return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait())) return StrResponse(url, HexUtil.encodeHexStr(getByteArrayAwait()))
} }
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
try {
setCookie(source?.getKey()) setCookie(source?.getKey())
val strResponse: StrResponse val strResponse: StrResponse
if (this.useWebView && useWebView) { if (this.useWebView && useWebView) {
@ -406,8 +407,10 @@ class AnalyzeUrl(
} }
} }
} }
fetchEnd(concurrentRecord)
return strResponse return strResponse
} finally {
fetchEnd(concurrentRecord)
}
} }
@JvmOverloads @JvmOverloads
@ -426,6 +429,7 @@ class AnalyzeUrl(
*/ */
suspend fun getResponseAwait(): Response { suspend fun getResponseAwait(): Response {
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
try {
setCookie(source?.getKey()) setCookie(source?.getKey())
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
val response = getProxyClient(proxy).newCallResponse(retry) { val response = getProxyClient(proxy).newCallResponse(retry) {
@ -447,8 +451,10 @@ class AnalyzeUrl(
else -> get(urlNoQuery, fieldMap, true) else -> get(urlNoQuery, fieldMap, true)
} }
} }
fetchEnd(concurrentRecord)
return response return response
} finally {
fetchEnd(concurrentRecord)
}
} }
fun getResponse(): Response { fun getResponse(): Response {
@ -460,16 +466,16 @@ class AnalyzeUrl(
/** /**
* 访问网站,返回ByteArray * 访问网站,返回ByteArray
*/ */
@Suppress("UnnecessaryVariable")
suspend fun getByteArrayAwait(): ByteArray { suspend fun getByteArrayAwait(): ByteArray {
val concurrentRecord = fetchStart() val concurrentRecord = fetchStart()
try {
@Suppress("RegExpRedundantEscape") @Suppress("RegExpRedundantEscape")
val dataUriFindResult = dataUriRegex.find(urlNoQuery) val dataUriFindResult = dataUriRegex.find(urlNoQuery)
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
if (dataUriFindResult != null) { if (dataUriFindResult != null) {
val dataUriBase64 = dataUriFindResult.groupValues[1] val dataUriBase64 = dataUriFindResult.groupValues[1]
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT) val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
fetchEnd(concurrentRecord)
return byteArray return byteArray
} else { } else {
setCookie(source?.getKey()) setCookie(source?.getKey())
@ -492,9 +498,11 @@ class AnalyzeUrl(
else -> get(urlNoQuery, fieldMap, true) else -> get(urlNoQuery, fieldMap, true)
} }
}.bytes() }.bytes()
fetchEnd(concurrentRecord)
return byteArray return byteArray
} }
} finally {
fetchEnd(concurrentRecord)
}
} }
fun getByteArray(): ByteArray { fun getByteArray(): ByteArray {
@ -503,49 +511,9 @@ class AnalyzeUrl(
} }
} }
/**
* 访问网站,返回InputStream
*/
suspend fun getInputStreamAwait(): InputStream {
val concurrentRecord = fetchStart()
@Suppress("RegExpRedundantEscape")
val dataUriFindResult = dataUriRegex.find(urlNoQuery)
@Suppress("BlockingMethodInNonBlockingContext")
if (dataUriFindResult != null) {
val dataUriBase64 = dataUriFindResult.groupValues[1]
val byteArray = Base64.decode(dataUriBase64, Base64.DEFAULT)
fetchEnd(concurrentRecord)
return ByteArrayInputStream(byteArray)
} else {
setCookie(source?.getKey())
val inputStream = getProxyClient(proxy).newCallResponseBody(retry) {
addHeaders(headerMap)
when (method) {
RequestMethod.POST -> {
url(urlNoQuery)
val contentType = headerMap["Content-Type"]
val body = body
if (fieldMap.isNotEmpty() || body.isNullOrBlank()) {
postForm(fieldMap, true)
} else if (!contentType.isNullOrBlank()) {
val requestBody = body.toRequestBody(contentType.toMediaType())
post(requestBody)
} else {
postJson(body)
}
}
else -> get(urlNoQuery, fieldMap, true)
}
}.byteStream()
fetchEnd(concurrentRecord)
return inputStream
}
}
fun getInputStream(): InputStream { fun getInputStream(): InputStream {
return runBlocking { return runBlocking {
getInputStreamAwait() getResponseAwait().body!!.byteStream()
} }
} }

@ -8,6 +8,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.AudioManager import android.media.AudioManager
import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
@ -85,6 +86,7 @@ abstract class BaseReadAloudService : BaseService(),
wakeLock.acquire() wakeLock.acquire()
isRun = true isRun = true
pause = false pause = false
observeLiveBus()
initMediaSession() initMediaSession()
initBroadcastReceiver() initBroadcastReceiver()
upNotification() upNotification()
@ -92,6 +94,15 @@ abstract class BaseReadAloudService : BaseService(),
setTimer(AppConfig.ttsTimer) setTimer(AppConfig.ttsTimer)
} }
fun observeLiveBus() {
observeEvent<Bundle>(EventBus.READ_ALOUD_PLAY) {
val play = it.getBoolean("play")
val pageIndex = it.getInt("pageIndex")
val startPos = it.getInt("startPos")
newReadAloud(play, pageIndex, startPos)
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
wakeLock.release() wakeLock.release()

@ -11,6 +11,7 @@ import io.legado.app.R
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern import io.legado.app.constant.AppPattern
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.entities.HttpTTS
import io.legado.app.exception.ConcurrentException import io.legado.app.exception.ConcurrentException
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
@ -20,9 +21,13 @@ import io.legado.app.model.ReadBook
import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.analyzeRule.AnalyzeUrl
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.Response import okhttp3.Response
import org.mozilla.javascript.WrappedException import org.mozilla.javascript.WrappedException
import java.io.File import java.io.File
import java.io.InputStream
import java.net.ConnectException import java.net.ConnectException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
@ -41,9 +46,9 @@ class HttpReadAloudService : BaseReadAloudService(),
private var speechRate: Int = AppConfig.speechRatePlay + 5 private var speechRate: Int = AppConfig.speechRatePlay + 5
private var downloadTask: Coroutine<*>? = null private var downloadTask: Coroutine<*>? = null
private var playIndexJob: Job? = null private var playIndexJob: Job? = null
private var downloadTaskIsActive = false
private var downloadErrorNo: Int = 0 private var downloadErrorNo: Int = 0
private var playErrorNo = 0 private var playErrorNo = 0
private val downloadTaskActiveLock = Mutex()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -71,7 +76,7 @@ class HttpReadAloudService : BaseReadAloudService(),
val file = getSpeakFileAsMd5(fileName) val file = getSpeakFileAsMd5(fileName)
if (file.exists()) { if (file.exists()) {
playAudio(file) playAudio(file)
} else if (!downloadTaskIsActive) { } else if (!downloadTaskActiveLock.isLocked) {
downloadAudio() downloadAudio()
} }
} }
@ -97,36 +102,49 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun downloadAudio() { private fun downloadAudio() {
launch {
downloadTask?.cancel() downloadTask?.cancel()
while (downloadTaskIsActive) {
//在线tts大部分只能单线程,等待上次访问结束
delay(100)
}
downloadTask = execute { downloadTask = execute {
downloadTaskActiveLock.withLock {
ensureActive()
removeCacheFile() removeCacheFile()
val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null") val httpTts = ReadAloud.httpTTS ?: throw NoStackTraceException("tts is null")
contentList.forEachIndexed { index, content -> contentList.forEachIndexed { index, content ->
ensureActive() ensureActive()
val fileName = md5SpeakFileName(content) val fileName = md5SpeakFileName(content)
val speakText = content.replace(AppPattern.notReadAloudRegex, "") val speakText = content.replace(AppPattern.notReadAloudRegex, "")
if (hasSpeakFile(fileName)) { //已经下载好的语音缓存 if (speakText.isEmpty()) {
if (index == nowSpeak) { AppLog.put("阅读段落内容为空,使用无声音频代替。\n朗读文本:$content")
val file = getSpeakFileAsMd5(fileName) createSilentSound(fileName)
playAudio(file) } else if (!hasSpeakFile(fileName)) {
} runCatching {
} else if (speakText.isEmpty()) { val inputStream = getSpeakStream(httpTts, speakText)
AppLog.put( if (inputStream != null) {
"阅读段落内容为空,使用无声音频代替。\n朗读文本:$content" createSpeakFile(fileName, inputStream)
) } else {
createSilentSound(fileName) createSilentSound(fileName)
}
}.onFailure {
when (it) {
is CancellationException -> Unit
else -> pauseReadAloud()
}
return@execute
}
}
if (index == nowSpeak) { if (index == nowSpeak) {
val file = getSpeakFileAsMd5(fileName) val file = getSpeakFileAsMd5(fileName)
playAudio(file) playAudio(file)
} }
return@forEachIndexed }
} else { }
runCatching { }.onError(IO) {
AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
}
}
private suspend fun getSpeakStream(httpTts: HttpTTS, speakText: String): InputStream? {
while (true) {
try {
val analyzeUrl = AnalyzeUrl( val analyzeUrl = AnalyzeUrl(
httpTts.url, httpTts.url,
speakText = speakText, speakText = speakText,
@ -136,14 +154,12 @@ class HttpReadAloudService : BaseReadAloudService(),
) )
var response = analyzeUrl.getResponseAwait() var response = analyzeUrl.getResponseAwait()
ensureActive() ensureActive()
httpTts.loginCheckJs?.takeIf { checkJs -> val checkJs = httpTts.loginCheckJs
checkJs.isNotBlank() if (checkJs?.isNotBlank() == true) {
}?.let { checkJs ->
response = analyzeUrl.evalJS(checkJs, response) as Response response = analyzeUrl.evalJS(checkJs, response) as Response
} }
httpTts.contentType?.takeIf { ct -> val ct = httpTts.contentType
ct.isNotBlank() if (ct?.isNotBlank() == true) {
}?.let { ct ->
response.headers["Content-Type"]?.let { contentType -> response.headers["Content-Type"]?.let { contentType ->
if (!contentType.matches(ct.toRegex())) { if (!contentType.matches(ct.toRegex())) {
throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string()) throw NoStackTraceException("TTS服务器返回错误:" + response.body!!.string())
@ -151,69 +167,46 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
} }
ensureActive() ensureActive()
response.body!!.bytes().let { bytes ->
val file = createSpeakFileAsMd5IfNotExist(fileName)
file.writeBytes(bytes)
if (index == nowSpeak) {
playAudio(file)
}
}
downloadErrorNo = 0 downloadErrorNo = 0
}.onFailure { return response.body!!.byteStream()
when (it) { } catch (e: Exception) {
is CancellationException -> Unit when (e) {
is ConcurrentException -> { is CancellationException -> throw e
delay(it.waitTime.toLong()) is ConcurrentException -> delay(e.waitTime.toLong())
downloadAudio()
}
is ScriptException, is WrappedException -> { is ScriptException, is WrappedException -> {
AppLog.put("js错误\n${it.localizedMessage}", it) AppLog.put("js错误\n${e.localizedMessage}", e)
toastOnUi("js错误\n${it.localizedMessage}") toastOnUi("js错误\n${e.localizedMessage}")
it.printOnDebug() e.printOnDebug()
cancel() throw e
pauseReadAloud()
} }
is SocketTimeoutException, is ConnectException -> { is SocketTimeoutException, is ConnectException -> {
downloadErrorNo++ downloadErrorNo++
if (downloadErrorNo > 5) { if (downloadErrorNo > 5) {
val msg = "tts超时或连接错误超过5次\n${it.localizedMessage}" val msg = "tts超时或连接错误超过5次\n${e.localizedMessage}"
AppLog.put(msg, it) AppLog.put(msg, e)
toastOnUi(msg) toastOnUi(msg)
pauseReadAloud() throw e
} else {
downloadAudio()
} }
} }
else -> { else -> {
downloadErrorNo++ downloadErrorNo++
val msg = "tts下载错误\n${it.localizedMessage}" val msg = "tts下载错误\n${e.localizedMessage}"
AppLog.put(msg, it) AppLog.put(msg, e)
it.printOnDebug() e.printOnDebug()
if (downloadErrorNo > 5) { if (downloadErrorNo > 5) {
AppLog.put("TTS服务器连续5次错误,已暂停阅读。") val msg1 = "TTS服务器连续5次错误,已暂停阅读。"
toastOnUi("TTS服务器连续5次错误,已暂停阅读。") AppLog.put(msg1)
pauseReadAloud() toastOnUi(msg1)
throw e
} else { } else {
AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$content") AppLog.put("TTS下载音频出错,使用无声音频代替。\n朗读文本:$speakText")
createSilentSound(fileName) break
if (index == nowSpeak) {
val file = getSpeakFileAsMd5(fileName)
playAudio(file)
} }
} }
} }
} }
} }
} return null
}
}.onStart {
downloadTaskIsActive = true
}.onError {
AppLog.put("朗读下载出错\n${it.localizedMessage}", it)
}.onFinally {
downloadTaskIsActive = false
}
}
} }
@Synchronized @Synchronized
@ -238,7 +231,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun createSilentSound(fileName: String) { private fun createSilentSound(fileName: String) {
val file = createSpeakFileAsMd5IfNotExist(fileName) val file = createSpeakFile(fileName)
file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes()) file.writeBytes(resources.openRawResource(R.raw.silent_sound).readBytes())
} }
@ -250,10 +243,18 @@ class HttpReadAloudService : BaseReadAloudService(),
return File("${ttsFolderPath}$name.mp3") return File("${ttsFolderPath}$name.mp3")
} }
private fun createSpeakFileAsMd5IfNotExist(name: String): File { private fun createSpeakFile(name: String): File {
return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3") return FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3")
} }
private fun createSpeakFile(name: String, inputStream: InputStream) {
FileUtils.createFileIfNotExist("${ttsFolderPath}$name.mp3").outputStream().use { out ->
inputStream.use {
it.copyTo(out)
}
}
}
/** /**
* 移除缓存文件 * 移除缓存文件
*/ */

@ -140,6 +140,9 @@ class MoreConfigDialog : DialogFragment() {
PreferKey.progressBarBehavior -> { PreferKey.progressBarBehavior -> {
postEvent(EventBus.UP_SEEK_BAR, true) postEvent(EventBus.UP_SEEK_BAR, true)
} }
PreferKey.noAnimScrollPage -> {
ReadBook.callBack?.upPageAnim()
}
} }
} }

@ -83,7 +83,6 @@ class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) {
private fun initData() = binding.run { private fun initData() = binding.run {
upPlayState() upPlayState()
upTimerText(BaseReadAloudService.timeMinute) upTimerText(BaseReadAloudService.timeMinute)
seekTimer.progress = BaseReadAloudService.timeMinute
cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
upTtsSpeechRateEnabled(!cbTtsFollowSys.isChecked) upTtsSpeechRateEnabled(!cbTtsFollowSys.isChecked)
upSeekTimer() upSeekTimer()
@ -170,7 +169,7 @@ class ReadAloudDialog : BaseDialogFragment(R.layout.dialog_read_aloud) {
if (BaseReadAloudService.timeMinute > 0) { if (BaseReadAloudService.timeMinute > 0) {
binding.seekTimer.progress = BaseReadAloudService.timeMinute binding.seekTimer.progress = BaseReadAloudService.timeMinute
} else { } else {
binding.seekTimer.progress = 0 binding.seekTimer.progress = AppConfig.ttsTimer
} }
} }
} }

@ -503,6 +503,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
pageDelegate = NoAnimPageDelegate(this) pageDelegate = NoAnimPageDelegate(this)
} }
} }
(pageDelegate as? ScrollPageDelegate)?.noAnim = AppConfig.noAnimScrollPage
} }
/** /**

@ -17,6 +17,8 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
//速度追踪器 //速度追踪器
private val mVelocity: VelocityTracker = VelocityTracker.obtain() private val mVelocity: VelocityTracker = VelocityTracker.obtain()
var noAnim: Boolean = false
override fun onAnimStart(animationSpeed: Int) { override fun onAnimStart(animationSpeed: Int) {
//惯性滚动 //惯性滚动
fling( fling(
@ -104,6 +106,10 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
if (readView.isAbortAnim) { if (readView.isAbortAnim) {
return return
} }
if (noAnim) {
curPage.scroll(calcNextPageOffset())
return
}
readView.setStartPoint(0f, 0f, false) readView.setStartPoint(0f, 0f, false)
startScroll(0, 0, 0, calcNextPageOffset(), animationSpeed) startScroll(0, 0, 0, calcNextPageOffset(), animationSpeed)
} }
@ -112,6 +118,10 @@ class ScrollPageDelegate(readView: ReadView) : PageDelegate(readView) {
if (readView.isAbortAnim) { if (readView.isAbortAnim) {
return return
} }
if (noAnim) {
curPage.scroll(calcPrevPageOffset())
return
}
readView.setStartPoint(0f, 0f, false) readView.setStartPoint(0f, 0f, false)
startScroll(0, 0, 0, calcPrevPageOffset(), animationSpeed) startScroll(0, 0, 0, calcPrevPageOffset(), animationSpeed)
} }

@ -122,14 +122,17 @@ class BackupConfigFragment : PreferenceFragment(),
it.setOnBindEditTextListener { editText -> it.setOnBindEditTextListener { editText ->
editText.text = AppConfig.webDavDir?.toEditable() editText.text = AppConfig.webDavDir?.toEditable()
} }
it.setOnPreferenceChangeListener { _, newValue -> }
(newValue as String).isNotBlank() findPreference<EditTextPreference>(PreferKey.webDavDeviceName)?.let {
it.setOnBindEditTextListener { editText ->
editText.text = AppConfig.webDavDeviceName?.toEditable()
} }
} }
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl)) upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount)) upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir) upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
upPreferenceSummary(PreferKey.webDavDeviceName, AppConfig.webDavDeviceName)
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore") findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
?.onLongClick { restoreDir.launch(); true } ?.onLongClick { restoreDir.launch(); true }

@ -4,6 +4,7 @@ package io.legado.app.utils
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
import com.jeremyliao.liveeventbus.core.Observable import com.jeremyliao.liveeventbus.core.Observable
@ -71,3 +72,27 @@ inline fun <reified EVENT> Fragment.observeEventSticky(
eventObservable<EVENT>(it).observeSticky(this, o) eventObservable<EVENT>(it).observeSticky(this, o)
} }
} }
inline fun <reified EVENT> LifecycleService.observeEvent(
vararg tags: String,
noinline observer: (EVENT) -> Unit
) {
val o = Observer<EVENT> {
observer(it)
}
tags.forEach {
eventObservable<EVENT>(it).observe(this, o)
}
}
inline fun <reified EVENT> LifecycleService.observeEventSticky(
vararg tags: String,
noinline observer: (EVENT) -> Unit
) {
val o = Observer<EVENT> {
observer(it)
}
tags.forEach {
eventObservable<EVENT>(it).observeSticky(this, o)
}
}

@ -1043,4 +1043,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1046,4 +1046,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1046,4 +1046,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1043,4 +1043,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1045,4 +1045,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1045,4 +1045,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -1046,4 +1046,6 @@
<string name="search_scope">搜索范围</string> <string name="search_scope">搜索范围</string>
<string name="toggle_search_scope">切换</string> <string name="toggle_search_scope">切换</string>
<string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string> <string name="sure_clear_search_history">是否确认清除所有搜索历史记录</string>
<string name="no_anim_scroll_page">禁用滚动点击动画</string>
<string name="webdav_device_name">设备名称</string>
</resources> </resources>

@ -34,6 +34,13 @@
app:allowDividerBelow="false" app:allowDividerBelow="false"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.EditTextPreference
android:key="webDavDeviceName"
android:title="@string/webdav_device_name"
app:allowDividerAbove="false"
app:allowDividerBelow="false"
app:iconSpaceReserved="false" />
<io.legado.app.lib.prefs.SwitchPreference <io.legado.app.lib.prefs.SwitchPreference
android:key="syncBookProgress" android:key="syncBookProgress"
android:defaultValue="true" android:defaultValue="true"

@ -136,6 +136,13 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isBottomBackground="true" /> app:isBottomBackground="true" />
<io.legado.app.lib.prefs.SwitchPreference
android:defaultValue="false"
android:key="noAnimScrollPage"
android:title="@string/no_anim_scroll_page"
app:iconSpaceReserved="false"
app:isBottomBackground="true" />
<io.legado.app.lib.prefs.Preference <io.legado.app.lib.prefs.Preference
android:key="clickRegionalConfig" android:key="clickRegionalConfig"
android:title="@string/click_regional_config" android:title="@string/click_regional_config"

Loading…
Cancel
Save